Compare commits

..

4 Commits

Author SHA1 Message Date
link2xt
ae4202a7aa Update flake.lock 2024-05-04 22:05:45 +00:00
link2xt
6b952690a0 Switch nixpkgs to darwin-cross branch 2024-05-04 22:04:13 +00:00
link2xt
90f70b6e7d ci: build macOS deltachat-rpc-server with Nix 2024-05-04 22:03:47 +00:00
link2xt
f417effa81 build(nix): allow cross-compilation for darwin 2024-05-04 22:03:44 +00:00
129 changed files with 2379 additions and 9223 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.79.0
RUSTUP_TOOLCHAIN: 1.78.0
steps:
- uses: actions/checkout@v4
with:
@@ -40,18 +40,6 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
npm_constants:
name: Check if node constants are up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Rebuild constants
run: npm run build:core:constants
- name: Check that constants are not changed
run: git diff --exit-code
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
@@ -95,11 +83,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.79.0
rust: 1.78.0
- os: windows-latest
rust: 1.79.0
rust: 1.78.0
- os: macos-latest
rust: 1.79.0
rust: 1.78.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest

View File

@@ -80,18 +80,17 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Setup rust target
run: rustup target add ${{ matrix.arch }}-apple-darwin
- name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_android:
@@ -266,141 +265,3 @@ jobs:
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
permissions:
id-token: write
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
run: |
cd deltachat-rpc-server/npm-package
python --version
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
ls -lah platform_package
for platform in ./platform_package/*; do npm pack "$platform"; done
npm pack
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
if-no-files-found: error
- name: Upload npm packets to the GitHub release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,38 +1,82 @@
name: "Publish @deltachat/jsonrpc-client"
name: "jsonrpc js client build"
on:
workflow_dispatch:
release:
types: [published]
pull_request:
push:
tags:
- "*"
- "!py-*"
jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
runs-on: ubuntu-20.04
permissions:
id-token: write
contents: read
steps:
- name: Install tree
run: sudo apt install tree
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
node-version: "18"
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pull Request ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
shell: bash
working-directory: deltachat-jsonrpc/typescript
run: |
npm run build
npm pack .
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v4
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: Post links to details
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

4
.gitignore vendored
View File

@@ -33,7 +33,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode
.vscode/launch.json
python/accounts.txt
python/all-testaccounts.txt
tmp/
@@ -50,4 +50,4 @@ result
# direnv
.envrc
.direnv
.direnv

View File

@@ -1,497 +1,5 @@
# Changelog
## [1.141.1] - 2024-06-27
### Fixes
- Update quota if it's stale, not fresh ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)).
- sql: Assign migration adding msgs.deleted a new number.
### Refactor
- mimefactory: Factor out header confidentiality policy ([#5715](https://github.com/deltachat/deltachat-core-rust/pull/5715)).
- Improve logging during SMTP/IMAP configuration.
## [1.141.0] - 2024-06-24
### API-Changes
- deltachat-jsonrpc: Add `get_chat_securejoin_qr_code()`.
- api!(deltachat-rpc-client): make {Account,Chat}.get_qr_code() return no SVG
This is a breaking change, old method is renamed into `get_qr_code_svg()`.
### Features / Changes
- Prefer references to fully downloaded messages for chat assignment ([#5645](https://github.com/deltachat/deltachat-core-rust/pull/5645)).
- Protect From name for verified chats and To names for encrypted chats ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
- Display vCard contact name in the message summary.
- Case-insensitive search for non-ASCII messages ([#5052](https://github.com/deltachat/deltachat-core-rust/pull/5052)).
- Remove subject prefix from ad-hoc group names ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)).
- Replace "Unnamed group" with "👥📧" to avoid translation.
- Sync `Config::MvboxMove` across devices ([#5680](https://github.com/deltachat/deltachat-core-rust/pull/5680)).
- Don't reveal profile data to a not yet verified contact ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
- Don't reveal profile data in MDNs ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
### Fixes
- Fetch existing messages for bots as `InFresh` ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
- Keep tombstones for two days before deleting ([#3685](https://github.com/deltachat/deltachat-core-rust/pull/3685)).
- Housekeeping: Delete MDNs and webxdc status updates for tombstones.
- Delete user-deleted messages on the server even if they show up on IMAP later.
- Do not send sync messages if bcc_self is disabled.
- Don't generate Config sync messages for unconfigured accounts.
- Do not require the Message to render MDN.
### CI
- Update Rust to 1.79.0.
### Documentation
- Remove outdated documentation comment from `send_smtp_messages`.
- Remove misleading configuration comment.
### Miscellaneous Tasks
- Update curve25519-dalek 4.1.x and suppress 3.2.0 warning.
- Update provider database.
### Refactor
- Deduplicate dependency versions ([#5691](https://github.com/deltachat/deltachat-core-rust/pull/5691)).
- Store public key instead of secret key for peer channels.
### Tests
- Image drafted as Viewtype::File is sent as is.
- python: Set delete_server_after=1 ("delete immediately") for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
- deltachat-rpc-client: Test that webxdc realtime data is not reordered on the sender.
- python: Wait for bot's DC_EVENT_IMAP_INBOX_IDLE before sending messages to it ([#5699](https://github.com/deltachat/deltachat-core-rust/pull/5699)).
## [1.140.2] - 2024-06-07
### API-Changes
- jsonrpc: Add set_draft_vcard(.., msg_id, contacts).
### Fixes
- Allow fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
- Remove group member locally even if send_msg() fails ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
- Revert member addition if the corresponding message couldn't be sent ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
- @deltachat/stdio-rpc-server: Make local non-symlinked installation possible by using absolute paths for local dev version ([#5679](https://github.com/deltachat/deltachat-core-rust/pull/5679)).
### Miscellaneous Tasks
- cargo: Bump schemars from 0.8.19 to 0.8.21.
- cargo: Bump backtrace from 0.3.71 to 0.3.72.
### Refactor
- @deltachat/stdio-rpc-server: Use old school require instead of the experimental json import ([#5628](https://github.com/deltachat/deltachat-core-rust/pull/5628)).
### Tests
- Set fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
- Don't leave protected group if some member's key is missing ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
## [1.140.1] - 2024-06-05
### Fixes
- Retry sending MDNs on temporary error.
- Set Config::IsChatmail in configure().
- Do not miss new messages while expunging the folder.
- Log messages with `info!` instead of `println!`.
### Documentation
- imap: Document why CLOSE is faster than EXPUNGE.
### Refactor
- imap: Make select_folder() accept non-optional folder.
- Improve SMTP logs and errors.
- Remove unused `select_folder::Error` variants.
### Tests
- deltachat-rpc-client: reenable `log_cli`.
## [1.140.0] - 2024-06-04
### Features / Changes
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
### Fixes
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
- Allow creation of groups by outgoing messages without recipients.
- Prefer `Chat-Group-ID` over references for new groups.
- Do not fail to send images with wrong extensions.
### Build system
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
### CI
- Remove cargo-nextest bug workaround.
### Documentation
- Add vCard as supported standard.
- Create_group() does not find chats, only creates them.
- Fix a typo in test_partial_group_consistency().
### Refactor
- Factor create_adhoc_group() call out of create_group().
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
### Tests
- Fix logging of TestContext created using TestContext::new_alice().
- Refactor `test_alias_*` into 8 separate tests.
## [1.139.6] - 2024-05-25
### Build system
- Update `iroh` to the git version.
- nix: Add iroh-base output hash.
- Upgrade iroh to 0.17.0.
### Fixes
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
- Acquire write lock on iroh_channels before checking for subscribe_loop.
### Miscellaneous Tasks
- Fix python lint.
- cargo-deny: Remove unused entry from deny.toml.
### Refactor
- Log IMAP connection type on connection failure.
### Tests
- Viewtype::File attachments are sent unchanged and preserve extensions.
- deltachat-rpc-client: Add realtime channel tests.
- deltachat-rpc-client: Regression test for double gossip subscription.
## [1.139.5] - 2024-05-23
### API-Changes
- deltachat-ffi: Make WebXdcRealtimeData data usable in CFFI.
- Add event channel overflow event.
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_DATA constant.
- deltachat-rpc-client: Add Message.send_webxdc_realtime_advertisement().
- deltachat-rpc-client: Add Message.send_webxdc_realtime_data().
### Features / Changes
- deltachat-repl: Add start-realtime and send-realtime commands.
### Fixes
- peer_channels: Connect to peers that advertise to you.
- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)).
### Tests
- peer_channels: Add test_parallel_connect().
- "SecureJoin wait" state and info messages.
## [1.139.4] - 2024-05-21
### Features / Changes
- Scale up contact origins to OutgoingTo when sending a message.
- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
### Fixes
- Do not log warning if iroh relay metadata is NIL.
- contact-tools: Parse_vcard: Support `\r\n` newlines.
- Make_vcard: Add authname and key for ContactId::SELF.
### Other
- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)).
## [1.139.3] - 2024-05-20
### API-Changes
- [**breaking**] @deltachat/stdio-rpc-server: change api: don't search in path unless `options.takeVersionFromPATH` is set to `true`
- @deltachat/stdio-rpc-server: remove `DELTA_CHAT_SKIP_PATH` environment variable
- @deltachat/stdio-rpc-server: remove version check / search for dc rpc server in $PATH
- @deltachat/stdio-rpc-server: remove `options.skipSearchInPath`
- @deltachat/stdio-rpc-server: add `options.takeVersionFromPATH`
- deltachat-rpc-client: Add Account.wait_for_incoming_msg().
### Features / Changes
- Replace env_logger with tracing_subscriber.
### Fixes
- Ignore event channel overflows.
- mimeparser: Take the last header of multiple ones with the same name.
- Db migration version 59, it contained an sql syntax error.
- Sql syntax error in db migration 27.
- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)).
- @deltachat/stdio-rpc-server: set default options for `startDeltaChat`.
- Always convert absolute paths to relative in accounts.toml.
### Refactor
- receive_imf: Do not check for ContactId::UNDEFINED.
- receive_imf: Remove unnecessary check for is_mdn.
- receive_imf: Only call create_or_lookup_group() with allow_creation=true.
- Use let..else in create_or_lookup_group().
- Stop trying to extract chat ID from Message-IDs.
- Do not try to lookup group in create_or_lookup_group().
## [1.139.2] - 2024-05-18
### Build system
- Add repository URL to @deltachat/jsonrpc-client.
## [1.139.1] - 2024-05-18
### CI
- Set `--access public` when publishing to npm.
## [1.139.0] - 2024-05-18
### Features / Changes
- Ephemeral peer channels ([#5346](https://github.com/deltachat/deltachat-core-rust/pull/5346)).
### Fixes
- Save override sender displayname for outgoing messages.
- Do not mark the message as seen if it has `location.kml`.
- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/deltachat/deltachat-core-rust/pull/5579)).
- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/deltachat/deltachat-core-rust/pull/5583)).
- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/deltachat/deltachat-core-rust/pull/5588))
- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/deltachat/deltachat-core-rust/pull/5589))
- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/deltachat/deltachat-core-rust/pull/5580))
### API-Changes
- deltachat-jsonrpc: Return vcard contact directly in MessageObject.
- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/deltachat/deltachat-core-rust/pull/5584)).
- deltachat-rpc-client: Add ViewType.VCARD constant.
- deltachat-rpc-client: Add Contact.make_vcard().
- deltachat-rpc-client: Add Chat.send_contact().
### CI
- Publish @deltachat/jsonrpc-client directly to npm.
- Check that constants are always up-to-date.
### Build system
- nix: Add git-cliff to flake.
- nix: Use rust-analyzer nightly
### Miscellaneous Tasks
- cargo: Downgrade libc from 0.2.154 to 0.2.153.
### Tests
- deltachat-rpc-client: Test sending vCard.
## [1.138.5] - 2024-05-16
### API-Changes
- jsonrpc: Add parse_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
- Add Viewtype::Vcard ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
- Add make_vcard() ([#5203](https://github.com/deltachat/deltachat-core-rust/pull/5203)).
### Build system
- Add repository URL to deltachat-rpc-server packages.
### Fixes
- Parsing vCards with avatars exported by Android's "Contacts" app.
### Miscellaneous Tasks
- Rebuild node constants.
### Refactor
- contact-tools: VcardContact: rename display_name to authname.
- VcardContact: Change timestamp type to i64.
## [1.138.4] - 2024-05-15
### CI
- Run actions/setup-node before npm publish.
## [1.138.3] - 2024-05-15
### CI
- Give CI job permission to publish binaries to the release.
## [1.138.2] - 2024-05-15
### API-Changes
- deltachat-rpc-client: Add CONFIG_SYNCED constant.
### CI
- Add npm token to publish deltachat-rpc-server packages.
### Features / Changes
- Reset more settings when configuring a chatmail account.
### Tests
- Set configuration after configure() finishes.
## [1.138.1] - 2024-05-14
### Features / Changes
- Detect XCHATMAIL capability and expose it as `is_chatmail` config.
### Fixes
- Never treat message with Chat-Group-ID as a private reply.
- Always prefer Chat-Group-ID over In-Reply-To and References.
- Ignore parent message if message references itself.
### CI
- Set RUSTUP_WINDOWS_PATH_ADD_BIN to work around `nextest` issue <https://github.com/nextest-rs/nextest/issues/1493>.
- deltachat-rpc-server: Fix upload of npm packages to github releases ([#5564](https://github.com/deltachat/deltachat-core-rust/pull/5564)).
### Refactor
- Add MimeMessage.get_chat_group_id().
- Make MimeMessage.get_header() return Option<&str>.
- sql: Make open flags immutable.
- Resultify token::lookup_or_new().
### Miscellaneous Tasks
- cargo: Bump parking_lot from 0.12.1 to 0.12.2.
- cargo: Bump libc from 0.2.153 to 0.2.154.
- cargo: Bump hickory-resolver from 0.24.0 to 0.24.1.
- cargo: Bump serde_json from 1.0.115 to 1.0.116.
- cargo: Bump human-panic from 1.2.3 to 2.0.0.
- cargo: Bump brotli from 5.0.0 to 6.0.0.
## [1.138.0] - 2024-05-13
### API-Changes
- Add dc_msg_save_file() which saves file copy at the provided path ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
- Api!(jsonrpc): replace EphemeralTimer tag "variant" with "kind"
### CI
- Use rsync instead of 3rd party github action.
- Replace `black` with `ruff format`.
- Update Rust to 1.78.0.
### Documentation
- Fix references in Message.set_location() documentation.
- Remove Doxygen markup from Message.has_location().
- Add `location` module documentation.
### Features / Changes
- Delete expired path locations in ephemeral loop.
- Delete orphaned POI locations during housekeeping.
- Parsing vCards for contacts sharing ([#5482](https://github.com/deltachat/deltachat-core-rust/pull/5482)).
- contact-tools: Support parsing profile images from "PHOTO:data:image/jpeg;base64,...".
- contact-tools: Add make_vcard().
- Do not add location markers to messages with non-POI location.
- Make one-to-one chats read-only the first seconds of a SecureJoin ([#5512](https://github.com/deltachat/deltachat-core-rust/pull/5512)).
### Fixes
- Message::set_file_from_bytes(): Set Param::Filename.
- Do not fail to send encrypted quotes to unencrypted chats.
- Never prepend subject to message text when bot receives it.
- Interrupt location loop when new location is stored.
- Correct message viewtype before recoding image blob ([#5496](https://github.com/deltachat/deltachat-core-rust/pull/5496)).
- Delete POI location when disappearing message expires.
- Delete non-POI locations after `delete_device_after`, not immediately.
- Update special chats icons even if they are blocked ([#5509](https://github.com/deltachat/deltachat-core-rust/pull/5509)).
- Use ChatIdBlocked::lookup_by_contact() instead of ChatId's method when applicable.
### Miscellaneous Tasks
- cargo: Bump quote from 1.0.35 to 1.0.36.
- cargo: Bump base64 from 0.22.0 to 0.22.1.
- cargo: Bump serde from 1.0.197 to 1.0.200.
- cargo: Bump async-channel from 2.2.0 to 2.2.1.
- cargo: Bump thiserror from 1.0.58 to 1.0.59.
- cargo: Bump anyhow from 1.0.81 to 1.0.82.
- cargo: Bump chrono from 0.4.37 to 0.4.38.
- cargo: Bump imap-proto from 0.16.4 to 0.16.5.
- cargo: Bump syn from 2.0.57 to 2.0.60.
- cargo: Bump mailparse from 0.14.1 to 0.15.0.
- cargo: Bump schemars from 0.8.16 to 0.8.19.
### Other
- Build ts docs with ci + nix.
- Push docs to delta.chat instead of codespeak
- Implement jsonrpc-docs build in github action
- Rm unneeded rust install from ts docs ci
- Correct folder for js.jsonrpc docs
- Add npm install to upload-docs.yml
- Add : to upload-docs.yml
- Upload-docs npm run => npm run build
- Rm leading slash
- Rm npm install
- Merge pull request #5515 from deltachat/dependabot/cargo/quote-1.0.36
- Merge pull request #5522 from deltachat/dependabot/cargo/chrono-0.4.38
- Merge pull request #5523 from deltachat/dependabot/cargo/mailparse-0.15.0
- Add webxdc internal integration commands in jsonrpc ([#5541](https://github.com/deltachat/deltachat-core-rust/pull/5541))
- Limit quote replies ([#5543](https://github.com/deltachat/deltachat-core-rust/pull/5543))
- Stdio jsonrpc server npm package ([#5332](https://github.com/deltachat/deltachat-core-rust/pull/5332))
### Refactor
- python: Fix ruff 0.4.2 warnings.
- Move `delete_poi_location` to location module and document it.
- Remove allow_keychange.
### Tests
- Explain test_was_seen_recently false-positive and give workaround instructions ([#5474](https://github.com/deltachat/deltachat-core-rust/pull/5474)).
- Test that member is added even if "Member added" is lost.
- Test that POIs are deleted when ephemeral message expires.
- Test ts build on branch
## [1.137.4] - 2024-04-24
### API-Changes
@@ -4475,21 +3983,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3
[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4
[1.138.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.4...v1.138.0
[1.138.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.0...v1.138.1
[1.138.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.1...v1.138.2
[1.138.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.2...v1.138.3
[1.138.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.3...v1.138.4
[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5
[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0
[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1
[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0
[1.140.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.0...v1.140.1
[1.140.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.1...v1.140.2
[1.141.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.2...v1.141.0
[1.141.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.0...v1.141.1

2175
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.141.1"
version = "1.137.4"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -40,53 +40,51 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.0"
async-channel = { workspace = true }
async-channel = "2.2.1"
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = { workspace = true }
brotli = { version = "6", default-features=false, features = ["std"] }
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
base64 = "0.22"
brotli = { version = "5", default-features=false, features = ["std"] }
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.9"
fd-lock = "4"
futures = { workspace = true }
futures-lite = { workspace = true }
futures = "0.3"
futures-lite = "2.3.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = "0.17.0"
iroh-gossip = { version = "0.17.0", features = ["net"] }
quinn = "0.10.0"
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mailparse = "0.15"
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = { workspace = true }
num-traits = "0.2"
once_cell = { workspace = true }
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.11", default-features = false }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.31"
quoted_printable = "0.5"
rand = { workspace = true }
rand = "0.8"
regex = { workspace = true }
reqwest = { version = "0.11.27", features = ["json"] }
reqwest = { version = "0.12.2", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sanitize-filename = "0.5"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1.13.2"
@@ -94,26 +92,35 @@ strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
thiserror = "1"
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio-util = "0.7.9"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
futures-lite = "2.3.0"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
tempfile = "3"
testdir = "0.9.0"
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
[workspace]
@@ -159,28 +166,9 @@ harness = false
[workspace.dependencies]
anyhow = "1"
ansi_term = "0.12.1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
futures = "0.3.30"
futures-lite = "2.3.0"
libc = "0.2"
log = "0.4"
num-traits = "0.2"
once_cell = "1.18.0"
rand = "0.8"
regex = "1.10"
rusqlite = "0.31"
sanitize-filename = "0.5"
serde_json = "1"
serde = "1.0"
tempfile = "3.10.1"
thiserror = "1"
tokio = "1.38.0"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.5.2"
rusqlite = { version = "0.31" }
[features]
default = ["vendored"]

View File

@@ -4,18 +4,21 @@ For example, to release version 1.116.0 of the core, do the following steps.
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
2. Run `npm run build:core:constants` in the root of the repository
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
3. add a link to compare previous with current version to the end of CHANGELOG.md:
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
4. add a link to compare previous with current version to the end of CHANGELOG.md:
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
6. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
6. Tag the release: `git tag -a v1.116.0`.
7. Tag the release: `git tag -a v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
8. Push the release tag: `git push origin v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;

View File

@@ -1,9 +1,10 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
version = "0.1.0"
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
description = "Contact-related tools, like parsing vcards and sanitizing name and address"
license = "MPL-2.0"
# TODO maybe it should be called "deltachat-text-utils" or similar?
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -12,7 +13,6 @@ anyhow = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -29,195 +29,10 @@ use std::fmt;
use std::ops::Deref;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use chrono::{DateTime, NaiveDateTime};
use once_cell::sync::Lazy;
use regex::Regex;
// TODOs to clean up:
// - Check if sanitizing is done correctly everywhere
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:{addr}\n\
FN:{display_name}\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\n");
}
res += "END:VCARD\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
let remainder = remove_prefix(s, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// TODO this doesn't handle the case where there are quotes around a colon
let (params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
Some(value)
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for line in lines.by_ref() {
if let Some(email) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some(name) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
{
key.get_or_insert(k);
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
{
photo.get_or_insert(p);
} else if let Some(rev) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
break;
}
}
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
}
contacts
}
/// Valid contact address.
#[derive(Debug, Clone)]
pub struct ContactAddress(String);
@@ -266,10 +81,14 @@ impl rusqlite::types::ToSql for ContactAddress {
/// Make the name and address
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
strip_rtlo_characters(
&captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str())),
)
} else {
strip_rtlo_characters(name)
},
@@ -278,21 +97,8 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(
strip_rtlo_characters(&normalize_name(name)),
addr.to_string(),
)
};
let mut name = normalize_name(&name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();
(strip_rtlo_characters(name), addr.to_string())
}
(name, addr)
}
/// Normalize a name.
@@ -424,124 +230,8 @@ impl rusqlite::types::ToSql for EmailAddress {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:alice@example.org\n\
FN:Alice Wonderland\n\
KEY:data:application/pgp-keys;base64,[base64-data]\n\
PHOTO:data:image/jpeg;base64,image in Base64\n\
REV:20240418T184242Z\n\
END:VCARD\n",
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:bob@example.com\n\
FN:bob@example.com\n\
REV:19700101T000000Z\n\
END:VCARD\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
@@ -587,85 +277,4 @@ END:VCARD",
assert!(EmailAddress::new("u@tt").is_ok());
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing.
// This one is Android-like.
let vcard0 = "BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
";
// This one is DOS-like.
let vcard1 = vcard0.replace('\n', "\r\n");
for vcard in [vcard0, vcard1.as_str()] {
let contacts = parse_vcard(vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.141.1"
version = "1.137.4"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -16,16 +16,16 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = { workspace = true }
human-panic = { version = "2", default-features = false }
num-traits = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
anyhow = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
once_cell = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose"] }
libc = "0.2"
human-panic = { version = "1", default-features = false }
num-traits = "0.2"
serde_json = "1.0"
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"
once_cell = "1.18.0"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features]
default = ["vendored"]

View File

@@ -481,9 +481,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages,
* accepts contact requests automatically (calling dc_accept_chat() is not needed),
* does not cut large incoming text messages,
* handles existing messages the same way as new ones if `fetch_existing_msgs=1`.
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
* and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
@@ -494,8 +493,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* For most bots calling `dc_markseen_msgs()` is the
* recommended way to update this value
* even for self-sent messages.
* - `fetch_existing_msgs` = 0=do not fetch existing messages on configure (default),
* 1=fetch most recent existing messages on configure.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
* 0=use IMAP IDLE if the server supports it.
@@ -518,15 +517,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -4395,9 +4390,9 @@ int dc_msg_has_deviating_timestamp(const dc_msg_t* msg);
/**
* Check if a message has a POI location bound to it.
* These locations are also returned by dc_get_locations()
* The UI may decide to display a special icon beside such messages.
* Check if a message has a location bound to it.
* These messages are also returned by dc_get_locations()
* and the UI may decide to display a special icon beside such messages,
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -5484,11 +5479,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_WEBXDC 80
/**
* Message containing shared contacts represented as a vCard (virtual contact file)
* with email addresses and possibly other fields.
*/
#define DC_MSG_VCARD 90
/**
* @}
@@ -6288,18 +6278,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
* Data received over an ephemeral peer channel.
*
* @param data1 (int) msg_id
* @param data2 (int) + (char*) binary data.
* length is returned as integer with dc_event_get_data2_int()
* and binary data is returned as dc_event_get_data2_str().
* Binary data must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
/**
* Tells that the Background fetch was completed (or timed out).
*
@@ -6328,14 +6306,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that some events have been skipped due to event channel overflow.
*
* @param data1 (int) number of events that have been skipped
*/
#define DC_EVENT_CHANNEL_OVERFLOW 2400
/**
* @}
*/
@@ -6649,8 +6619,6 @@ void dc_event_unref(dc_event_t* event);
///
/// Used as message text of outgoing read receipts.
/// - %1$s will be replaced by the subject of the displayed message
///
/// @deprecated Deprecated 2024-06-23, use DC_STR_READRCPT_MAILBODY2 instead.
#define DC_STR_READRCPT_MAILBODY 32
/// @deprecated Deprecated, this string is no longer needed.
@@ -7359,24 +7327,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/// "Establishing guaranteed end-to-end encryption, please wait…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "The message is a receipt notification."
///
/// Used as message text of outgoing read receipts.
#define DC_STR_READRCPT_MAILBODY2 192
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200
/**
* @}
*/

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
#![warn(unused, clippy::all)]
#![allow(
non_camel_case_types,
@@ -562,11 +561,9 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ConfigSynced { .. } => 2111,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::WebxdcRealtimeData { .. } => 2150,
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::EventChannelOverflow { .. } => 2400,
}
}
@@ -619,13 +616,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
EventType::EventChannelOverflow { n } => *n as libc::c_int,
}
}
@@ -664,9 +659,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::EventChannelOverflow { .. } => 0,
| EventType::ConfigSynced { .. } => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
@@ -681,7 +675,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
}
}
@@ -732,8 +725,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
| EventType::ChatlistChanged => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -749,11 +741,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::WebxdcRealtimeData { data, .. } => {
let ptr = libc::malloc(data.len());
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
ptr as *mut libc::c_char
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.141.1"
version = "1.137.4"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -13,30 +13,29 @@ path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
anyhow = { workspace = true }
anyhow = "1"
deltachat = { path = ".." }
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
num-traits = { workspace = true }
schemars = "0.8.21"
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
log = { workspace = true }
async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
num-traits = "0.2"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.10.1"
log = "0.4"
async-channel = { version = "2.2.1" }
futures = { version = "0.3.30" }
serde_json = "1"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
tokio = { version = "1.37.0" }
sanitize-filename = "0.5"
walkdir = "2.5.0"
base64 = { workspace = true }
base64 = "0.22"
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.3", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -1,6 +1,5 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::str;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
@@ -18,14 +17,12 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
@@ -34,7 +31,6 @@ use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -46,7 +42,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::contact::{ContactObject, VcardContact};
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
@@ -188,16 +184,6 @@ impl CommandApi {
self.accounts.write().await.add_account().await
}
/// Imports/migrated an existing account from a database path into this account manager.
/// Returns the ID of new account.
async fn migrate_account(&self, path_to_db: String) -> Result<u32> {
self.accounts
.write()
.await
.migrate_account(std::path::PathBuf::from(path_to_db))
.await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts
.write()
@@ -342,11 +328,6 @@ impl CommandApi {
ctx.get_info().await
}
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
@@ -707,22 +688,7 @@ impl CommandApi {
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
///
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
/// If `chat_id` is unset, setup contact QR code is returned.
async fn get_chat_securejoin_qr_code(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
Ok(qr)
}
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
@@ -744,9 +710,10 @@ impl CommandApi {
) -> Result<(String, String)> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
let svg = get_securejoin_qr_svg(&ctx, chat).await?;
Ok((qr, svg))
Ok((
securejoin::get_securejoin_qr(&ctx, chat).await?,
get_securejoin_qr_svg(&ctx, chat).await?,
))
}
/// Continue a Setup-Contact or Verified-Group-Invite protocol
@@ -1459,51 +1426,6 @@ impl CommandApi {
Ok(contact_id.map(|id| id.to_u32()))
}
/// Parses a vCard file located at the given path. Returns contacts in their original order.
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
let vcard = fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat_contact_tools::parse_vcard(vcard)
.into_iter()
.map(|c| c.into())
.collect())
}
/// Imports contacts from a vCard file located at the given path.
///
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let vcard = tokio::fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat::contact::import_vcard(&ctx, vcard)
.await?
.into_iter()
.map(|c| c.to_u32())
.collect())
}
/// Returns a vCard containing contacts with the given ids.
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
deltachat::contact::make_vcard(&ctx, &contacts).await
}
/// Sets vCard containing the given contacts to the message draft.
async fn set_draft_vcard(
&self,
account_id: u32,
msg_id: u32,
contacts: Vec<u32>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
msg.make_vcard(&ctx, &contacts).await?;
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -1808,37 +1730,6 @@ impl CommandApi {
.await
}
async fn send_webxdc_realtime_data(
&self,
account_id: u32,
instance_msg_id: u32,
data: Vec<u8>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
send_webxdc_realtime_data(&ctx, MsgId::new(instance_msg_id), data).await
}
async fn send_webxdc_realtime_advertisement(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
if let Some(fut) = fut {
tokio::spawn(async move {
fut.await.ok();
info!(ctx, "send_webxdc_realtime_advertisement done")
});
}
Ok(())
}
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
}
async fn get_webxdc_status_updates(
&self,
account_id: u32,
@@ -1880,29 +1771,6 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Sets Webxdc file as integration.
/// `file` is the .xdc to use as Webxdc integration.
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.set_webxdc_integration(&file_path).await
}
/// Returns Webxdc instance used for optional integrations.
/// UI can open the Webxdc as usual.
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
/// `integrate_for` is the chat to get the integration for.
async fn init_webxdc_integration(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.init_webxdc_integration(chat_id.map(ChatId::new))
.await?
.map(|msg_id| msg_id.to_u32()))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -88,35 +87,3 @@ impl ContactObject {
})
}
}
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VcardContact {
/// Email address.
addr: String,
/// The contact's name, or the email address if no name was given.
display_name: String,
/// Public PGP key in Base64.
key: Option<String>,
/// Profile image in Base64.
profile_image: Option<String>,
/// Contact color as hex string.
color: String,
/// Last update timestamp.
timestamp: Option<i64>,
}
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,
key: vc.key,
profile_image: vc.profile_image,
color: color_int_to_hex_string(color),
timestamp: vc.timestamp.ok(),
}
}
}

View File

@@ -240,10 +240,6 @@ pub enum EventType {
status_update_serial: u32,
},
/// Data received over an ephemeral peer channel.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted { msg_id: u32 },
@@ -263,9 +259,6 @@ pub enum EventType {
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow { n: u64 },
}
impl From<CoreEventType> for EventType {
@@ -369,10 +362,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
msg_id: msg_id.to_u32(),
data,
},
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
@@ -381,7 +370,6 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.map(|id| id.to_u32()),
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
}
}
}

View File

@@ -1,4 +1,3 @@
use crate::api::VcardContact;
use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
@@ -36,10 +35,6 @@ pub struct MessageObject {
parent_id: Option<u32>,
text: String,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -88,8 +83,6 @@ pub struct MessageObject {
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
@@ -176,13 +169,6 @@ impl MessageObject {
Some(reactions.into())
};
let vcard_contacts: Vec<VcardContact> = message
.vcard_contacts(context)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
@@ -242,8 +228,6 @@ impl MessageObject {
download_state,
reactions,
vcard_contact: vcard_contacts.first().cloned(),
})
}
}
@@ -286,11 +270,6 @@ pub enum MessageViewtype {
/// Message is an webxdc instance.
Webxdc,
/// Message containing shared contacts represented as a vCard (virtual contact file)
/// with email addresses and possibly other fields.
/// Use `parse_vcard()` to retrieve them.
Vcard,
}
impl From<Viewtype> for MessageViewtype {
@@ -307,7 +286,6 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
}
}
}
@@ -326,7 +304,6 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,
}
}
}
@@ -365,14 +342,6 @@ pub enum SystemMessageType {
LocationOnly,
InvalidUnencryptedMail,
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
/// to complete.
SecurejoinWait,
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
/// send messages.
SecurejoinWaitTimeout,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
@@ -391,9 +360,6 @@ pub enum SystemMessageType {
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
/// This message contains a users iroh node address.
IrohNodeAddr,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -416,9 +382,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
}
}
}
@@ -668,7 +631,7 @@ impl MessageInfo {
#[derive(
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum EphemeralTimer {
/// Timer is disabled.
Disabled,

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
pub mod api;
pub use yerpc;

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::net::SocketAddr;
use std::path::PathBuf;

View File

@@ -25,17 +25,12 @@
"exports": {
".": {
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs",
"types": "./dist/deltachat.d.ts"
"require": "./dist/deltachat.cjs"
}
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
@@ -58,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.141.1"
"version": "1.137.4"
}

View File

@@ -1,20 +1,20 @@
[package]
name = "deltachat-repl"
version = "1.141.1"
version = "1.137.4"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true }
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = { workspace = true }
rusqlite = { workspace = true }
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rustyline = "14"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
[features]
default = ["vendored"]

View File

@@ -19,7 +19,6 @@ use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -643,30 +642,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
let msg_id = MsgId::new(arg1.parse()?);
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
if let Some(res) = res {
println!("waiting for peer channel join");
res.await?;
}
println!("joined peer channel");
}
"send-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
if arg2.is_empty() {
bail!("no message");
}
let msg_id = MsgId::new(arg1.parse()?);
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
println!("sent realtime message");
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
bail!("Argument [chat-id] is missing.");

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
//! This is a CLI program and a little testing frame. This file must not be
//! included when using Delta Chat Core as a library.
//!
@@ -32,7 +31,6 @@ use rustyline::{
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing_subscriber::EnvFilter;
mod cmdline;
use self::cmdline::*;
@@ -484,10 +482,9 @@ async fn handle_cmd(
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
)
pretty_env_logger::formatted_timed_builder()
.parse_default_env()
.filter_module("deltachat_repl", log::LevelFilter::Info)
.init();
let args = std::env::args().collect();

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.141.1"
version = "1.137.4"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -250,16 +250,12 @@ class Account:
"""
return Chat(self, self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> str:
"""Get Setup-Contact QR Code text.
def get_qr_code(self) -> tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
This data needs to be transferred to another Delta Chat account
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
return self._rpc.get_chat_securejoin_qr_code(self.id, None)
def get_qr_code_svg(self) -> tuple[str, str]:
"""Get Setup-Contact QR code text and SVG."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
@@ -301,12 +297,6 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event."""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import calendar
from dataclasses import dataclass
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
@@ -96,11 +95,7 @@ class Chat:
"""Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> str:
"""Get Join-Group QR code text."""
return self._rpc.get_chat_securejoin_qr_code(self.account.id, self.id)
def get_qr_code_svg(self) -> tuple[str, str]:
def get_qr_code(self) -> tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
@@ -270,11 +265,3 @@ class Chat:
location["message"] = Message(self.account, location.msg_id)
locations.append(location)
return locations
def send_contact(self, contact: Contact):
"""Send contact to the chat."""
vcard = contact.make_vcard()
with NamedTemporaryFile(suffix=".vcard") as f:
f.write(vcard.encode())
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})

View File

@@ -61,8 +61,6 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
class ChatId(IntEnum):
@@ -115,7 +113,6 @@ class ViewType(str, Enum):
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
class SystemMessageType(str, Enum):

View File

@@ -60,6 +60,3 @@ class Contact:
self.account,
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)
def make_vcard(self) -> str:
return self._rpc.make_vcard(self.account.id, [self.id])

View File

@@ -2,7 +2,7 @@ import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict, futuremethod
from ._utils import AttrDict
from .const import EventType
from .contact import Contact
@@ -70,11 +70,3 @@ class Message:
event = self.account.wait_for_event()
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break
@futuremethod
def send_webxdc_realtime_advertisement(self):
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data) -> None:
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))

View File

@@ -126,7 +126,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
alice.set_config("download_limit", "1")
msg = bob.wait_for_incoming_msg()
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
@@ -134,15 +135,13 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
)
message = alice.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.download_state == const.DownloadState.AVAILABLE
msg_id = alice.wait_for_incoming_msg_event().msg_id
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
snapshot = message.get_snapshot()
chat_id = snapshot.chat_id
alice._rpc.download_full_message(alice.id, message.id)
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
alice._rpc.download_full_message(alice.id, msg_id)
wait_for_chatlist_specific_item(alice, chat_id)
@@ -178,7 +177,8 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
alice_chat_bob.send_text("hello")
msg = bob.wait_for_incoming_msg()
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
bob_chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
@@ -189,7 +189,8 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
# make sure alice_second_device already received the message
alice_second_device.wait_for_incoming_msg_event()
msg = alice.wait_for_incoming_msg()
event = alice.wait_for_incoming_msg_event()
msg = alice.get_message_by_id(event.msg_id)
alice_second_device.clear_all_events()
msg.mark_seen()

View File

@@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""
Testing webxdc iroh connectivity
If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import sys
import threading
import time
import pytest
from deltachat_rpc_client import EventType
@pytest.fixture()
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
assert p.exists()
return str(p)
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "play"
# send iroh announcements simultaneously
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
return ac1_webxdc_msg, ac2_webxdc_msg
def setup_thread_send_realtime_data(msg, data):
def thread_run():
for _i in range(10):
msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
def wait_receive_realtime_data(msg_data_list):
account = msg_data_list[0][0].account
msg_data_list = msg_data_list[:]
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
while msg_data_list:
event = account.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
for i, (msg, data) in enumerate(msg_data_list):
if msg.id == event.msg_id:
assert list(data) == event.data
log(f"msg {msg.id}: got correct realtime data {data}")
del msg_data_list[i]
break
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping1"
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(b"foo")
break
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
ac2_webxdc_msg.get_snapshot().chat.accept()
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
# Issue a "send" call in parallel with sending advertisement.
# Previously due to a bug this caused subscribing to the channel twice.
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
def thread_run():
for i in range(10):
data = str(i).encode()
ac1_webxdc_msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
n = int(bytes(event.data).decode())
break
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert int(bytes(event.data).decode()) > n
break
def test_no_reordering(acfactory, path_to_webxdc):
"""Test that sending a lot of realtime messages does not result in reordering."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
for i in range(200):
ac1_webxdc_msg.send_webxdc_realtime_data([i])
for i in range(200):
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
if event.data[0] == i:
break
pytest.fail("Reordering detected")

View File

@@ -7,7 +7,7 @@ from deltachat_rpc_client import Chat, EventType, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
@@ -46,7 +46,7 @@ def test_qr_securejoin(acfactory, protect):
assert alice_chat.get_basic_snapshot().is_protected == protect
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
# Check that at least some of the handshake messages are deleted.
@@ -91,7 +91,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = bob.wait_for_event()
@@ -106,7 +106,7 @@ def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
charlie.secure_join(qr_code)
@@ -168,13 +168,13 @@ def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
@@ -188,7 +188,7 @@ def test_verified_group_recovery(acfactory) -> None:
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -205,7 +205,7 @@ def test_verified_group_recovery(acfactory) -> None:
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -252,7 +252,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -269,7 +269,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -336,7 +336,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code = ac2.get_qr_code()
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
@@ -346,7 +346,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code = ch1.get_qr_code()
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -359,7 +359,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code = ch1.get_qr_code()
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
@@ -381,7 +381,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code = vg.get_qr_code()
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
@@ -402,7 +402,7 @@ def test_qr_new_group_unblocked(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code = ac1_chat.get_qr_code()
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -425,7 +425,7 @@ def test_aeap_flow_verified(acfactory):
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -464,12 +464,12 @@ def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# Bob verifies Alice.
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code = carol.get_qr_code()
qr_code, _svg = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
@@ -520,16 +520,16 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac3_chat = ac3.create_group("Verified group", protect=True)
# ac1 joins ac3 group.
ac3_qr_code = ac3_chat.get_qr_code()
ac3_qr_code, _svg = ac3_chat.get_qr_code()
ac1.secure_join(ac3_qr_code)
ac1.wait_for_securejoin_joiner_success()
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
ac1_qr_code = snapshot.chat.get_qr_code()
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
# ac2 verifies ac1
qr_code = ac1.get_qr_code()
qr_code, _svg = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -589,7 +589,7 @@ def test_withdraw_securejoin_qr(acfactory):
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()

View File

@@ -508,8 +508,8 @@ def test_reactions_for_a_reordering_move(acfactory):
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
ac2.set_config("mvbox_move", "1")
ac2.configure()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()

View File

@@ -1,15 +0,0 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.141.1"
version = "1.137.4"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -13,15 +13,15 @@ categories = ["cryptography", "std", "email"]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = { workspace = true }
futures-lite = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["io-std"] }
tokio-util = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
anyhow = "1"
env_logger = { version = "0.11.3" }
futures-lite = "2.3.0"
log = "0.4"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.37.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]
default = ["vendored"]

View File

@@ -1,3 +0,0 @@
platform_package
*.tgz
package-lock.json

View File

@@ -1,3 +0,0 @@
platform_package/*
scripts/
*.tgz

View File

@@ -1,78 +0,0 @@
## npm package for deltachat-rpc-server
This is the successor of `deltachat-node`,
it does not use NAPI bindings but instead uses stdio executables
to let you talk to core over jsonrpc over stdio.
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
## Usage
> The **minimum** nodejs version for this package is `16`
```
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
```
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close()
}
main()
```
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
## How to use on an unsupported platform
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
## How does it work when you install it
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
references:
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
When you import this package it searches for the rpc server in the following locations and order:
1. `DELTA_CHAT_RPC_SERVER` environment variable
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
3. prebuilds in npm packages
so by default it uses the prebuilds.
## How do you built this package in CI
- To build platform packages, run the `build_platform_package.py` script:
```
python3 build_platform_package.py <cargo-target>
# example
python3 build_platform_package.py x86_64-apple-darwin
```
- Then pass it as an artifact to the last CI action that publishes the main package.
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
- then publish `deltachat-rpc-server/npm-package`,
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
## How to build a version you can use localy on your host machine for development
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
- If you just need your host platform run `python scripts/make_local_dev_version.py`
- note: this clears the `platform_package` folder
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
## Thanks to nlnet
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.

View File

@@ -1,42 +0,0 @@
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
export interface SearchOptions {
/** whether take deltachat-rpc-server inside of $PATH*/
takeVersionFromPATH: boolean;
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
disableEnvPath: boolean;
}
/**
*
* @returns absolute path to deltachat-rpc-server binary
* @throws when it is not found
*/
export function getRPCServerPath(
options?: Partial<SearchOptions>
): Promise<string>;
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
readonly pathToServerBinary: string;
};
export interface StartOptions {
/** whether to disable outputting stderr to the parent process's stderr */
muteStdErr: boolean;
}
/**
*
* @param directory directory for accounts folder
* @param options
*/
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
export namespace FnTypes {
export type getRPCServerPath = typeof getRPCServerPath
export type startDeltaChat = typeof startDeltaChat
}

View File

@@ -1,108 +0,0 @@
//@ts-check
import { spawn } from "node:child_process";
import { stat } from "node:fs/promises";
import os from "node:os";
import process from "node:process";
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
import {
ENV_VAR_LOCATION_NOT_FOUND,
FAILED_TO_START_SERVER_EXECUTABLE,
NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR,
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
} from "./src/errors.js";
import { createRequire } from "node:module";
function findRPCServerInNodeModules() {
const arch = os.arch();
const operating_system = process.platform;
const package_name = `@deltachat/stdio-rpc-server-${operating_system}-${arch}`;
try {
const { resolve } = createRequire(import.meta.url);
return resolve(package_name);
} catch (error) {
console.debug("findRpcServerInNodeModules", error);
const require = createRequire(import.meta.url);
if (
Object.keys(require("./package.json").optionalDependencies).includes(
package_name
)
) {
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
} else {
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
}
}
}
/** @type {import("./index").FnTypes.getRPCServerPath} */
export async function getRPCServerPath(options = {}) {
const { takeVersionFromPATH, disableEnvPath } = {
takeVersionFromPATH: false,
disableEnvPath: false,
...options,
};
// 1. check if it is set as env var
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
try {
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
throw new Error(
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
);
}
} catch (error) {
throw new Error(ENV_VAR_LOCATION_NOT_FOUND());
}
return process.env[ENV_VAR_NAME];
}
// 2. check if PATH should be used
if (takeVersionFromPATH) {
return PATH_EXECUTABLE_NAME;
}
// 3. check for prebuilds
return findRPCServerInNodeModules();
}
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
/** @type {import("./index").FnTypes.startDeltaChat} */
export async function startDeltaChat(directory, options = {}) {
const pathToServerBinary = await getRPCServerPath(options);
const server = spawn(pathToServerBinary, {
env: {
RUST_LOG: process.env.RUST_LOG,
DC_ACCOUNTS_PATH: directory,
},
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
});
server.on("error", (err) => {
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
//@ts-expect-error
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
dc.close = () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
};
//@ts-expect-error
dc.pathToServerBinary = pathToServerBinary;
return dc;
}

View File

@@ -1,19 +0,0 @@
{
"license": "MPL-2.0",
"main": "index.js",
"name": "@deltachat/stdio-rpc-server",
"optionalDependencies": {},
"peerDependencies": {
"@deltachat/jsonrpc-client": "*"
},
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"prepack": "node scripts/update_optional_dependencies_and_version.js"
},
"type": "module",
"types": "index.d.ts",
"version": "1.141.1"
}

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env python3
import subprocess
from sys import argv
from os import path, makedirs, chdir
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 2:
print("First argument should be target architecture as required by cargo")
exit(1)
target = argv[1].strip()
subprocess.run(
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
check=True,
)
newpath = "platform_package"
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = "platform_package/" + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
def binary_path(binary_name):
return "../../target/" + target + "/release/" + binary_name
my_binary_name = "deltachat-rpc-server"
if not path.isfile(binary_path("deltachat-rpc-server")):
my_binary_name = "deltachat-rpc-server.exe"
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
print("Did not find the build")
exit(1)
my_binary_path = binary_path(my_binary_name)
copy(my_binary_path, platform_path + "/" + my_binary_name)
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -1,34 +0,0 @@
# This script is for making a version of the npm packet that you can install locally
import subprocess
from sys import argv
from os import path, makedirs, chdir
import re
import json
import tomllib
from shutil import copy, rmtree
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
# get host target with "rustc -vV"
output = subprocess.run(["rustc", "-vV"], capture_output=True)
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
print("host target to build for is:", host_target)
# clean platform_package folder
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
else:
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
makedirs(newpath)
# run build_platform_package.py with the host's target to build it
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)
# typescript / npm local package installing/linking needs that this package has it's own node_modules folder
subprocess.run(["npm", "i"], capture_output=False, check=True)

View File

@@ -1,46 +0,0 @@
import subprocess
from sys import argv
from os import path, makedirs, chdir, chmod, stat
import json
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 3:
print("First argument should be target architecture as required by cargo")
print("Second argument should be the location of th built binary (binary_path)")
exit(1)
target = argv[1].strip()
binary_path = argv[2].strip()
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
available_targets = output.stdout.decode("utf-8")
if available_targets.find(target) == -1:
print("target", target, "is not known / not valid")
exit(1)
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = 'platform_package/' + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
my_binary_name = path.basename(binary_path)
new_binary_path = platform_path + "/" + my_binary_name
copy(binary_path, new_binary_path)
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -1,21 +0,0 @@
def convert_cpu_arch_to_npm_cpu_arch(arch):
if arch == "x86_64":
return "x64"
if arch == "i686":
return "ia32"
if arch == "aarch64":
return "arm64"
if arch == "armv7" or arch == "arm":
return "arm"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
return arch
def convert_os_to_npm_os(os):
if os == "windows":
return "win32"
if os == "darwin" or os == "linux":
return os
if os.startswith("android"):
return "android"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
return os

View File

@@ -1,34 +0,0 @@
import tomllib
import json
from .convert_platform import convert_cpu_arch_to_npm_cpu_arch, convert_os_to_npm_os
def write_package_json(platform_path, rust_target, my_binary_name):
if len(rust_target.split("-")) == 3:
[cpu_arch, vendor, os] = rust_target.split("-")
else:
[cpu_arch, vendor, os, _env] = rust_target.split("-")
# read version
tomlfile = open("../../Cargo.toml", 'rb')
version = tomllib.load(tomlfile)['package']['version']
package_json = {
"name": "@deltachat/stdio-rpc-server-"
+ convert_os_to_npm_os(os)
+ "-"
+ convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
"version": version,
"os": [convert_os_to_npm_os(os)],
"cpu": [convert_cpu_arch_to_npm_cpu_arch(cpu_arch)],
"main": my_binary_name,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git",
},
}
file = open(platform_path + "/package.json", 'w')
file.write(json.dumps(package_json, indent=4))

View File

@@ -1,64 +0,0 @@
import fs from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
if (process.cwd() !== expected_cwd) {
console.error(
"CWD missmatch: this script needs to be run from " + expected_cwd,
{ actual: process.cwd(), expected: expected_cwd }
);
process.exit(1);
}
// whether to use local paths instead of npm registry version number for the prebuilds in optionalDependencies
// useful for local development
const is_local = process.argv.includes("--local");
const package_json = JSON.parse(await fs.readFile("./package.json", "utf8"));
const cargo_toml = await fs.readFile("../Cargo.toml", "utf8");
const version = cargo_toml
.split("\n")
.find((line) => line.includes("version"))
.split('"')[1];
const platform_packages_dir = "./platform_package";
const platform_package_names = await Promise.all(
(await fs.readdir(platform_packages_dir)).map(async (name) => {
const p = JSON.parse(
await fs.readFile(
join(platform_packages_dir, name, "package.json"),
"utf8"
)
);
if (p.version !== version) {
console.error(
name,
"has a different version than the version of the rpc server.",
{ rpc_server: version, platform_package: p.version }
);
throw new Error("version missmatch");
}
return { folder_name: name, package_name: p.name };
})
);
package_json.version = version;
package_json.optionalDependencies = {};
for (const { folder_name, package_name } of platform_package_names) {
package_json.optionalDependencies[package_name] = is_local
? `file:${expected_cwd}/platform_package/${folder_name}` // npm seems to work better with an absolute path here
: version;
}
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
}
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));

View File

@@ -1,5 +0,0 @@
//@ts-check
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"

View File

@@ -1,41 +0,0 @@
//@ts-check
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:
- Install it with "npm i ${package_name}"
- or download/compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR() {
return `deltachat-rpc-server not found:
Unfortunately no prebuild is available for your system, so you need to provide deltachat-rpc-server yourself.
- Download or Compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function ENV_VAR_LOCATION_NOT_FOUND(error) {
return `deltachat-rpc-server not found in ${ENV_VAR_NAME}:
Error: ${error}
Content of ${ENV_VAR_NAME}: "${process.env[ENV_VAR_NAME]}"`;
}
export function FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, error) {
return `Failed to start server executable at '${pathToServerBinary}',
Error: ${error}
Make sure the deltachat-rpc-server binary exists at this location
and you can start it with \`${pathToServerBinary} --version\``;
}

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
//! Delta Chat core RPC server.
//!
//! It speaks JSON Lines over stdio.
@@ -11,7 +10,6 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tracing_subscriber::EnvFilter;
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
@@ -29,9 +27,6 @@ async fn main() {
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
if let Err(error) = &r {
log::error!("Fatal error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
@@ -64,13 +59,7 @@ async fn main_impl() -> Result<()> {
#[cfg(target_family = "unix")]
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interferring with JSON-RPC using stdout.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);

View File

@@ -15,10 +15,6 @@ ignore = [
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Problem in curve25519-dalek 3.2.0 used by iroh 0.4.
# curve25519-dalek 4.1.3 has the problem fixed.
"RUSTSEC-2024-0344",
]
[bans]
@@ -27,9 +23,6 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "asn1-rs-derive", version = "0.4.0" },
{ name = "asn1-rs-impl", version = "0.1.0" },
{ name = "asn1-rs", version = "0.5.2" },
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
@@ -41,77 +34,46 @@ skip = [
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der_derive", version = "0.6.1" },
{ name = "derive_more", version = "0.99.17" },
{ name = "der-parser", version = "8.2.0" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "dlopen2", version = "0.4.1" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "0.10.2" },
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "hyper-rustls", version = "0.24.2" },
{ name = "hyper", version = "0.14.28" },
{ name = "idna", version = "0.4.0" },
{ name = "netlink-packet-core", version = "0.5.0" },
{ name = "netlink-packet-route", version = "0.15.0" },
{ name = "nix", version = "0.26.4" },
{ name = "oid-registry", version = "0.6.1" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pem", version = "1.1.1" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "rcgen", version = "<0.12.1" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "reqwest", version = "0.11.27" },
{ name = "ring", version = "0.16.20" },
{ name = "rustls-pemfile", version = "1.0.4" },
{ name = "rustls", version = "0.21.11" },
{ name = "rustls-webpki", version = "0.101.7" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "ssh-encoding", version = "0.1.0" },
{ name = "ssh-key", version = "0.5.1" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "synstructure", version = "0.12.6" },
{ name = "syn", version = "1.0.109" },
{ name = "system-configuration-sys", version = "0.5.0" },
{ name = "system-configuration", version = "0.5.1" },
{ name = "time", version = "<0.3" },
{ name = "tokio-rustls", version = "0.24.1" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "webpki-roots", version ="0.25.4" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
{ name = "windows-core", version = "<0.54.0" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "0.32.0" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
{ name = "x509-parser", version = "<0.16.0" },
]

41
flake.lock generated
View File

@@ -48,11 +48,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"lastModified": 1713421495,
"narHash": "sha256-5vVF9W1tJT+WdfpWAEG76KywktKDAW/71mVmNHEHjac=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
"rev": "fd47b1f9404fae02a4f38bd9f4b12bad7833c96b",
"type": "github"
},
"original": {
@@ -166,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"lastModified": 1713248628,
"narHash": "sha256-NLznXB5AOnniUtZsyy/aPWOk8ussTuePp2acb9U+ISA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"rev": "5672bc9dbf9d88246ddab5ac454e82318d094bb8",
"type": "github"
},
"original": {
@@ -182,11 +182,12 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
"lastModified": 1713562564,
"narHash": "sha256-NQpYhgoy0M89g9whRixSwsHb8RFIbwlxeYiVSDwSXJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "92d295f588631b0db2da509f381b4fb1e74173c5",
"type": "github"
},
"original": {
"id": "nixpkgs",
@@ -195,16 +196,16 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1714076141,
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
"owner": "nixos",
"lastModified": 1714698973,
"narHash": "sha256-GcLKCUJ+TfOisWVi9ZsdsSASPxT0qk8NTN2wXnvkyho=",
"owner": "reckenrode",
"repo": "nixpkgs",
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
"rev": "0479d3c5baf4b4dd0528941f21b34c06fdfb3ed4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"owner": "reckenrode",
"ref": "darwin-cross",
"repo": "nixpkgs",
"type": "github"
}
@@ -222,11 +223,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"lastModified": 1713373173,
"narHash": "sha256-octd9BFY9G/Gbr4KfwK4itZp4Lx+qvJeRRcYnN+dEH8=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"rev": "46702ffc1a02a2ac153f1d1ce619ec917af8f3a6",
"type": "github"
},
"original": {

View File

@@ -5,7 +5,7 @@
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs.url = "github:reckenrode/nixpkgs/darwin-cross";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
@@ -241,6 +241,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
@@ -355,6 +358,8 @@
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkRustPackages "x86_64-darwin" //
mkRustPackages "aarch64-darwin" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
@@ -525,26 +530,15 @@
};
};
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in pkgs.mkShell {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo
clippy
rustc
rustfmt
rust-analyzer
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
}

View File

@@ -30,7 +30,6 @@ module.exports = {
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_CHANNEL_OVERFLOW: 2400,
DC_EVENT_CHATLIST_CHANGED: 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
@@ -67,7 +66,6 @@ module.exports = {
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
@@ -112,7 +110,6 @@ module.exports = {
DC_MSG_IMAGE: 20,
DC_MSG_STICKER: 23,
DC_MSG_TEXT: 10,
DC_MSG_VCARD: 90,
DC_MSG_VIDEO: 50,
DC_MSG_VIDEOCHAT_INVITATION: 70,
DC_MSG_VOICE: 41,
@@ -176,7 +173,6 @@ module.exports = {
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
DC_STR_CONTACT: 200,
DC_STR_CONTACT_NOT_VERIFIED: 36,
DC_STR_CONTACT_SETUP_CHANGED: 37,
DC_STR_CONTACT_VERIFIED: 35,
@@ -266,13 +262,10 @@ module.exports = {
DC_STR_REACTED_BY: 177,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_READRCPT_MAILBODY2: 192,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECUREJOIN_WAIT: 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT: 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
DC_STR_SECURE_JOIN_REPLIES: 118,
DC_STR_SECURE_JOIN_STARTED: 117,

View File

@@ -37,9 +37,7 @@ module.exports = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
}

View File

@@ -30,7 +30,6 @@ export enum C {
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_CHANNEL_OVERFLOW = 2400,
DC_EVENT_CHATLIST_CHANGED = 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
@@ -67,7 +66,6 @@ export enum C {
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
@@ -112,7 +110,6 @@ export enum C {
DC_MSG_IMAGE = 20,
DC_MSG_STICKER = 23,
DC_MSG_TEXT = 10,
DC_MSG_VCARD = 90,
DC_MSG_VIDEO = 50,
DC_MSG_VIDEOCHAT_INVITATION = 70,
DC_MSG_VOICE = 41,
@@ -176,7 +173,6 @@ export enum C {
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT = 200,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
@@ -266,13 +262,10 @@ export enum C {
DC_STR_REACTED_BY = 177,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_READRCPT_MAILBODY2 = 192,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECUREJOIN_WAIT = 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT = 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
@@ -341,9 +334,7 @@ export const EventId2EventName: { [key: number]: string } = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
}

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.141.1"
"version": "1.137.4"
}

View File

@@ -44,6 +44,11 @@ def test_group_tracking_plugin(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
botproc.fnmatch_lines(
"""
*ac_configure_completed*
""",
)
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.141.1"
version = "1.137.4"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"

View File

@@ -275,7 +275,6 @@ class ACSetup:
def __init__(self, testprocess, init_time) -> None:
self._configured_events = Queue()
self._account2state: Dict[Account, str] = {}
self._account2config: Dict[Account, Dict[str, str]] = {}
self._imap_cleaned: Set[str] = set()
self.testprocess = testprocess
self.init_time = init_time
@@ -337,8 +336,6 @@ class ACSetup:
if not success:
pytest.fail(f"configuring online account {acc} failed: {comment}")
self._account2state[acc] = self.CONFIGURED
if acc in self._account2config:
acc.update_config(self._account2config[acc])
return acc
def _onconfigure_start_io(self, acc):
@@ -526,7 +523,6 @@ class ACFactory:
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
ac.update_config(configdict)
self._acsetup._account2config[ac] = configdict
self._preconfigure_key(ac, configdict["addr"])
return ac
@@ -552,15 +548,6 @@ class ACFactory:
bot_cfg = self.get_next_liveconfig()
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
self._acsetup.start_configure(bot_ac)
self.wait_configured(bot_ac)
bot_ac.start_io()
# Wait for DC_EVENT_IMAP_INBOX_IDLE so that all emails appeared in the bot's Inbox later are
# considered new and not existing ones, and thus processed by the bot.
print(bot_ac._logid, "waiting for inbox IDLE to become ready")
bot_ac._evtracker.wait_idle_inbox_ready()
bot_ac.stop_io()
self._acsetup._account2state[bot_ac] = self._acsetup.IDLEREADY
# Forget ac as it will be opened by the bot subprocess
# but keep something in the list to not confuse account generation

View File

@@ -1,5 +1,4 @@
import sys
import time
import pytest
import deltachat as dc
@@ -676,17 +675,3 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert ac2_offl_ac1_contact.is_verified()
def test_deleted_msgs_dont_reappear(acfactory):
ac1 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
ac1.set_config("bcc_self", "1")
chat = ac1.get_self_contact().create_chat()
msg = chat.send_text("hello")
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
ac1.delete_messages([msg])
ac1._evtracker.get_matching("DC_EVENT_MSG_DELETED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
time.sleep(5)
assert len(chat.get_messages()) == 0

View File

@@ -1 +1 @@
2024-06-27
2024-04-24

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.79.0
RUST_VERSION=1.78.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -149,7 +149,7 @@ def process_data(data, file):
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += (

View File

@@ -66,11 +66,7 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
json_list = [
"package.json",
"deltachat-jsonrpc/typescript/package.json",
"deltachat-rpc-server/npm-package/package.json",
]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=828e5ddc7e6609b582fbd7f063cc3f60b580ce96
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -485,6 +485,10 @@ impl Config {
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
let dir = file
.parent()
.context("Cannot get config file directory")?
.to_path_buf();
let mut config = Self::new_nosync(file, writable).await?;
let bytes = fs::read(&config.file)
.await
@@ -496,13 +500,9 @@ impl Config {
// Convert them to relative paths.
let mut modified = false;
for account in &mut config.inner.accounts {
if account.dir.is_absolute() {
if let Some(old_path_parent) = account.dir.parent() {
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
account.dir = new_path.to_path_buf();
modified = true;
}
}
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
if modified && writable {

View File

@@ -14,6 +14,7 @@ use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::tools::time;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
@@ -28,28 +29,45 @@ pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres).await
compute_dkim_results(context, authres, &from_domain, message_time).await
}
#[derive(Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
/// i.e. whether we expect DKIM to work.
pub dkim_should_work: bool,
/// Whether changing the public Autocrypt key should be allowed.
/// This is false if we expected DKIM to work (dkim_works=true),
/// but it failed now (dkim_passed=false).
pub allow_keychange: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
write!(
fmt,
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
self.dkim_passed, self.dkim_should_work, self.allow_keychange
)?;
if !self.allow_keychange {
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
}
Ok(())
}
}
@@ -200,6 +218,10 @@ async fn update_authservid_candidates(
context
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
.await?;
// Updating the authservid candidates may mean that we now consider
// emails as "failed" which "passed" previously, so we need to
// reset our expectation which DKIMs work.
clear_dkim_works(context).await?
}
Ok(())
}
@@ -216,6 +238,8 @@ async fn update_authservid_candidates(
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
from_domain: &str,
message_time: i64,
) -> Result<DkimResults> {
let mut dkim_passed = false;
@@ -248,7 +272,71 @@ async fn compute_dkim_results(
}
}
Ok(DkimResults { dkim_passed })
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
if message_time > last_working_timestamp && dkim_passed {
set_dkim_works_timestamp(context, from_domain, message_time).await?;
dkim_should_work = true;
}
Ok(DkimResults {
dkim_passed,
dkim_should_work,
allow_keychange: dkim_passed || !dkim_should_work,
})
}
/// Whether DKIM in emails from this domain should be considered to work.
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
// When we get an email with valid DKIM-Authentication-Results,
// then we assume that DKIM works for 30 days from this time on.
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
let dkim_ever_worked = last_working_timestamp > 0;
// We're using time() here and not the time when the message
// claims to have been sent (passed around as `message_time`)
// because otherwise an attacker could just put a time way
// in the future into the `Date` header and then we would
// assume that DKIM doesn't have to be valid anymore.
let dkim_should_work_now = should_work_until > time();
Ok(dkim_ever_worked && dkim_should_work_now)
}
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
let last_working_timestamp: i64 = context
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
(from_domain,),
)
.await?
.unwrap_or(0);
Ok(last_working_timestamp)
}
async fn set_dkim_works_timestamp(
context: &Context,
from_domain: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
(from_domain, timestamp),
)
.await?;
Ok(())
}
async fn clear_dkim_works(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM sending_domains", ())
.await?;
Ok(())
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
@@ -261,12 +349,19 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::join_securejoin;
use crate::test_utils;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
@@ -479,8 +574,33 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from).await?;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
}
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
println!(
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
entry.path()
);
test_failed = true;
}
let from_domain = EmailAddress::new(from).unwrap().domain;
assert_eq!(
res.dkim_should_work,
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
);
assert_eq!(res.dkim_passed, res.dkim_should_work);
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
@@ -493,8 +613,9 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!(
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
entry.path(),
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
test_failed = true;
}
@@ -517,7 +638,116 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
let bytes = b"From: invalid@from.com
Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
handle_authres(&t, &mail, "invalid@rom.com", time())
.await
.unwrap();
}
#[ignore = "Disallowing keychanges is disabled for now"]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob sends Alice a message, so she gets his key
tcm.send_recv_accept(&bob, &alice, "Hi").await;
// We don't need bob anymore, let's make sure it's not accidentally used
drop(bob);
// Assume Alice receives an email from bob@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
// And Alice knows her server's authserv-id
alice
.set_config(Config::AuthservIdCandidates, Some("example.org"))
.await?;
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
// Sleep to make sure key reset is ignored because of DKIM failure
// and not because reordering is suspected.
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob2 = tcm.unconfigured().await;
bob2.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob2).await?;
let chat = bob2.create_chat(&alice).await;
let mut sent = bob2
.send_text(chat.id, "Please send me lots of money")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
let received = alice.recv_msg(&sent).await;
// Assert that the error tells the user about the problem
assert!(received.error.unwrap().contains("DKIM failed"));
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
.await?
.unwrap();
// Encryption preference is still mutual.
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
// Also check that the keypair was not changed
assert_eq!(
bob_state.public_key.unwrap(),
test_utils::bob_keypair().public
);
// Since Alice didn't change the key, Bob can't read her message
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob2.ctx, &qr).await.unwrap();
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
} else {
break;
}
}
// Unfortunately, securejoin currently doesn't work with authres-checking,
// so these checks would fail:
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
// assert_eq!(
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
// assert_eq!(
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// // Bob can read Alice's messages again
// let received = tcm
// .try_send_recv(&alice, &bob2, "Can you read this again?")
// .await;
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
// assert!(received.error.is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -566,7 +796,10 @@ Authentication-Results: dkim=";
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob knows his server's authserv-id
// Assume Bob received an email from something@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
// And Bob knows his server's authserv-id
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
.await?;
@@ -588,13 +821,15 @@ Authentication-Results: dkim=";
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(rcvd
.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false"));
.contains("KEYCHANGES NOT ALLOWED"));
Ok(())
}

View File

@@ -3,7 +3,7 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::{Cursor, Seek};
use std::io::Cursor;
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
@@ -12,7 +12,6 @@ use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::codecs::jpeg::JpegEncoder;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
@@ -427,25 +426,9 @@ impl<'a> BlobObject<'a> {
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
let mut file = std::fs::File::open(self.to_abs_path())?;
let (nr_bytes, exif) = image_metadata(&file)?;
let (nr_bytes, exif) = self.metadata()?;
*no_exif_ref = exif.is_none();
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
let imgreader = match imgreader {
Ok(ir) => ir,
_ => {
file.rewind()?;
ImageReader::with_format(
std::io::BufReader::new(&file),
ImageFormat::from_path(&blob_abs)?,
)
}
};
let fmt = imgreader.format().context("No format??")?;
let mut img = imgreader.decode().context("image decode failure")?;
let mut img = image::open(&blob_abs).context("image decode failure")?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
@@ -474,9 +457,10 @@ impl<'a> BlobObject<'a> {
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
ImageFormat::Jpeg => {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
add_white_bg = false;
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
@@ -513,7 +497,7 @@ impl<'a> BlobObject<'a> {
img_wh = max(img.width(), img.height());
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
// crate when recoding, so don't scale them down.
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
@@ -554,7 +538,7 @@ impl<'a> BlobObject<'a> {
if do_scale || exif.is_some() {
// The file format is JPEG/PNG now, we may have to change the file extension
if !matches!(fmt, ImageFormat::Jpeg)
if !matches!(fmt, Ok(ImageFormat::Jpeg))
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
{
blob_abs = blob_abs.with_extension("jpg");
@@ -591,14 +575,15 @@ impl<'a> BlobObject<'a> {
}
}
}
}
/// Returns image file size and Exif.
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
/// Returns image file size and Exif.
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
let file = std::fs::File::open(self.to_abs_path())?;
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(&file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
@@ -1101,34 +1086,32 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
"jpg",
true, // has Exif
1000,
1000,
0,
1000,
1000,
)
.await
.unwrap();
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "1",
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
"jpg",
true, // has Exif
1000,
1000,
0,
1000,
1000,
)
.await
.unwrap();
}
@@ -1137,20 +1120,18 @@ mod tests {
async fn test_recode_image_2() {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
extension: "jpg",
has_exif: true,
original_width: 2000,
original_height: 1800,
orientation: 270,
compressed_width: 1800,
compressed_height: 2000,
..Default::default()
}
.test()
"jpg",
true, // has Exif
2000,
1800,
270,
1800,
2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
@@ -1159,18 +1140,18 @@ mod tests {
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
let bytes = buf.into_inner();
let img_rotated = SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "1",
bytes: &bytes,
extension: "jpg",
original_width: 1800,
original_height: 2000,
compressed_width: 1800,
compressed_height: 2000,
..Default::default()
}
.test()
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
&bytes,
"jpg",
false, // no Exif
1800,
2000,
0,
1800,
2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
@@ -1180,80 +1161,49 @@ mod tests {
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../test-data/image/screenshot.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: 1920,
compressed_height: 1080,
..Default::default()
}
.test()
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "1",
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: constants::WORSE_IMAGE_SIZE,
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
..Default::default()
}
.test()
.await
.unwrap();
SendImageCheckMediaquality {
viewtype: Viewtype::File,
media_quality_config: "1",
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: 1920,
compressed_height: 1080,
..Default::default()
}
.test()
.await
.unwrap();
SendImageCheckMediaquality {
viewtype: Viewtype::File,
media_quality_config: "1",
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: 1920,
compressed_height: 1080,
set_draft: true,
..Default::default()
}
.test()
"png",
false, // no Exif
1920,
1080,
0,
constants::WORSE_IMAGE_SIZE,
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
media_quality_config: "0",
send_image_check_mediaquality(
Viewtype::Sticker,
Some("0"),
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: 1920,
compressed_height: 1080,
..Default::default()
}
.test()
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
}
@@ -1264,18 +1214,18 @@ mod tests {
async fn test_recode_image_rgba_png_to_jpeg() {
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "1",
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
extension: "png",
original_width: 1920,
original_height: 1080,
compressed_width: constants::WORSE_IMAGE_SIZE,
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
..Default::default()
}
.test()
"png",
false, // no Exif
1920,
1080,
0,
constants::WORSE_IMAGE_SIZE,
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
}
@@ -1283,19 +1233,18 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
extension: "jpg",
has_exif: true,
original_width: 1920,
original_height: 1080,
compressed_width: constants::BALANCED_IMAGE_SIZE,
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
..Default::default()
}
.test()
"jpg",
true, // has Exif
1920,
1080,
0,
constants::BALANCED_IMAGE_SIZE,
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
}
@@ -1317,93 +1266,68 @@ mod tests {
assert_eq!(luma, 0);
}
#[derive(Default)]
struct SendImageCheckMediaquality<'a> {
pub(crate) viewtype: Viewtype,
pub(crate) media_quality_config: &'a str,
pub(crate) bytes: &'a [u8],
pub(crate) extension: &'a str,
pub(crate) has_exif: bool,
pub(crate) original_width: u32,
pub(crate) original_height: u32,
pub(crate) orientation: i32,
pub(crate) compressed_width: u32,
pub(crate) compressed_height: u32,
pub(crate) set_draft: bool,
}
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
viewtype: Viewtype,
media_quality_config: Option<&str>,
bytes: &[u8],
extension: &str,
has_exif: bool,
original_width: u32,
original_height: u32,
orientation: i32,
compressed_width: u32,
compressed_height: u32,
) -> anyhow::Result<DynamicImage> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file").with_extension(extension);
impl SendImageCheckMediaquality<'_> {
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
let viewtype = self.viewtype;
let media_quality_config = self.media_quality_config;
let bytes = self.bytes;
let extension = self.extension;
let has_exif = self.has_exif;
let original_width = self.original_width;
let original_height = self.original_height;
let orientation = self.orientation;
let compressed_width = self.compressed_width;
let compressed_height = self.compressed_height;
let set_draft = self.set_draft;
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(Config::MediaQuality, Some(media_quality_config))
.await?;
let file = alice.get_blobdir().join("file").with_extension(extension);
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
} else {
assert!(exif.is_none());
}
let mut msg = Message::new(viewtype);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
if set_draft {
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
assert_eq!(msg.get_viewtype(), Viewtype::File);
}
let sent = alice.send_msg(chat.id, &mut msg).await;
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
let file_saved = alice
.get_blobdir()
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
alice_msg.save_file(&alice, &file_saved).await?;
check_image_size(file_saved, compressed_width, compressed_height);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file_saved = bob
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
if viewtype == Viewtype::File {
assert_eq!(file_saved.extension().unwrap(), extension);
let bytes1 = fs::read(&file_saved).await?;
assert_eq!(&bytes1, bytes);
}
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
let blob = BlobObject::new_from_path(&alice, &file).await?;
let (_, exif) = blob.metadata()?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
} else {
assert!(exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);
Ok(img)
}
let mut msg = Message::new(viewtype);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
let file_saved = alice
.get_blobdir()
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
alice_msg.save_file(&alice, &file_saved).await?;
check_image_size(file_saved, compressed_width, compressed_height);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file_saved = bob
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
let (_, exif) = blob.metadata()?;
assert!(exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);
Ok(img)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1435,7 +1359,8 @@ mod tests {
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
let (file_size, _) = blob.metadata()?;
assert_eq!(file_size, bytes.len() as u64);
check_image_size(file_saved, width, height);
Ok(())

View File

@@ -12,7 +12,6 @@ use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
@@ -39,7 +38,6 @@ use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::securejoin::BobState;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
@@ -49,6 +47,7 @@ use crate::tools::{
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
smeared_time, time, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::WEBXDC_SUFFIX;
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -127,10 +126,6 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
/// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending
/// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed.
SecurejoinWait,
}
impl fmt::Display for CantSendReason {
@@ -150,7 +145,6 @@ impl fmt::Display for CantSendReason {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
}
}
}
@@ -291,7 +285,7 @@ impl ChatId {
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
.await
.map(|chat| chat.id)?;
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
chat_id
} else {
warn!(
@@ -488,7 +482,7 @@ impl ChatId {
// went to "contact requests" list rather than normal chatlist.
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != ContactId::SELF {
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
.await?;
}
}
@@ -616,10 +610,7 @@ impl ChatId {
let sort_to_bottom = true;
let ts = self
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, false)
.await?
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
// in case of race conditions.
.saturating_add(1);
.await?;
self.set_protection_for_timestamp_sort(context, protect, ts, contact_id)
.await
}
@@ -893,20 +884,8 @@ impl ChatId {
.await?
.context("no file stored in params")?;
msg.param.set(Param::File, blob.as_name());
if msg.viewtype == Viewtype::File {
if let Some((better_type, _)) =
message::guess_msgtype_from_suffix(&blob.to_abs_path())
// We do not do an automatic conversion to other viewtypes here so that
// users can send images as "files" to preserve the original quality
// (usually we compress images). The remaining conversions are done by
// `prepare_msg_blob()` later.
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
{
msg.viewtype = better_type;
}
}
if msg.viewtype == Viewtype::Vcard {
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
if blob.suffix() == Some(WEBXDC_SUFFIX) {
msg.viewtype = Viewtype::Webxdc;
}
}
}
@@ -927,13 +906,12 @@ impl ChatId {
.sql
.execute(
"UPDATE msgs
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
WHERE id=?;",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
@@ -957,11 +935,10 @@ impl ChatId {
type,
state,
txt,
txt_normalized,
param,
hidden,
mime_in_reply_to)
VALUES (?,?,?,?,?,?,?,?,?,?);",
VALUES (?,?,?, ?,?,?,?,?,?);",
(
self,
ContactId::SELF,
@@ -969,7 +946,6 @@ impl ChatId {
msg.viewtype,
MessageState::OutDraft,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
@@ -1431,18 +1407,6 @@ impl ChatId {
Ok(sort_timestamp)
}
/// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
/// and otherwise notifying the user accordingly.
pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
let context = context.clone();
task::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout)).await;
let chat = Chat::load_from_db(&context, self).await?;
chat.check_securejoin_wait(&context, 0).await?;
Result::<()>::Ok(())
});
}
}
impl std::fmt::Display for ChatId {
@@ -1622,12 +1586,6 @@ impl Chat {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
Some(NotAMember)
} else if self
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?
> 0
{
Some(SecurejoinWait)
} else {
None
};
@@ -1641,69 +1599,6 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
/// If the timeout has expired, notifies the user that sending messages is possible. See also
/// [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
timeout: u64,
) -> Result<u64> {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok(0);
}
let (mut param0, mut param1) = (Params::new(), Params::new());
param0.set_cmd(SystemMessage::SecurejoinWait);
param1.set_cmd(SystemMessage::SecurejoinWaitTimeout);
let (param0, param1) = (param0.to_string(), param1.to_string());
let Some((param, ts_sort, ts_start)) = context
.sql
.query_row_optional(
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
(self.id, &param0, &param1),
|row| {
let param: String = row.get(0)?;
let ts_sort: i64 = row.get(1)?;
let ts_start: i64 = row.get(2)?;
Ok((param, ts_sort, ts_start))
},
)
.await?
else {
return Ok(0);
};
if param == param1 {
return Ok(0);
}
let now = time();
// Don't await SecureJoin if the clock was set back.
if ts_start <= now {
let timeout = ts_start
.saturating_add(timeout.try_into()?)
.saturating_sub(now);
if timeout > 0 {
return Ok(timeout as u64);
}
}
add_info_msg_with_cmd(
context,
self.id,
&stock_str::securejoin_wait_timeout(context).await,
SystemMessage::SecurejoinWaitTimeout,
// Use the sort timestamp of the "please wait" message, this way the added message is
// never sorted below the protection message if the SecureJoin finishes in parallel.
ts_sort,
Some(now),
None,
None,
)
.await?;
context.emit_event(EventType::ChatModified(self.id));
Ok(0)
}
/// Checks if the user is part of a chat
/// and has basically the permissions to edit the chat therefore.
/// The function does not check if the chat type allows editing of concrete elements.
@@ -1946,10 +1841,6 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
let is_bot = context.get_config_bool(Config::Bot).await?;
msg.param
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
// Set "References:" to identify the "thread" of the conversation.
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
@@ -2078,7 +1969,7 @@ impl Chat {
.execute(
"UPDATE msgs
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
state=?, txt=?, txt_normalized=?, subject=?, param=?,
state=?, txt=?, subject=?, param=?,
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
ephemeral_timestamp=?
@@ -2092,7 +1983,6 @@ impl Chat {
msg.viewtype,
msg.state,
msg.text,
message::normalize_text(&msg.text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -2121,7 +2011,6 @@ impl Chat {
type,
state,
txt,
txt_normalized,
subject,
param,
hidden,
@@ -2133,7 +2022,7 @@ impl Chat {
location_id,
ephemeral_timer,
ephemeral_timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
params_slice![
msg.rfc724_mid,
msg.chat_id,
@@ -2143,7 +2032,6 @@ impl Chat {
msg.viewtype,
msg.state,
msg.text,
message::normalize_text(&msg.text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -2347,9 +2235,8 @@ pub struct ChatInfo {
}
pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
{
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Some(chat_id) = ChatId::lookup_by_contact(context, ContactId::SELF).await? {
let icon = include_bytes!("../assets/icon-saved-messages.png");
let blob = BlobObject::create(context, "icon-saved-messages.png", icon).await?;
let icon = blob.as_name().to_string();
@@ -2362,9 +2249,8 @@ pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()>
}
pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
{
// if there is no device-chat, there is nothing to update. this is no error.
if let Some(chat_id) = ChatId::lookup_by_contact(context, ContactId::DEVICE).await? {
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png", icon).await?;
let icon = blob.as_name().to_string();
@@ -2415,9 +2301,7 @@ async fn update_special_chat_name(
contact_id: ContactId,
name: String,
) -> Result<()> {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
{
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
// the `!= name` condition avoids unneeded writes
context
.sql
@@ -2446,26 +2330,6 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
Ok(())
}
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
/// unblocking the chat and notifying the user accordingly.
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
let Some(bobstate) = BobState::from_db(&context.sql).await? else {
return Ok(());
};
if !bobstate.in_progress() {
return Ok(());
}
let chat_id = bobstate.alice_chat();
let chat = Chat::load_from_db(context, chat_id).await?;
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
chat_id.spawn_securejoin_wait(context, timeout);
}
Ok(())
}
/// Handle a [`ChatId`] and its [`Blocked`] status at once.
///
/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
@@ -2640,7 +2504,6 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.get_blob(Param::File, context, !msg.is_increation())
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let send_as_is = msg.viewtype == Viewtype::File;
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
@@ -2666,14 +2529,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.await?;
}
if msg.viewtype == Viewtype::Vcard {
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
}
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
if !send_as_is
&& (msg.viewtype == Viewtype::Image
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
if msg.viewtype == Viewtype::Image
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
{
blob.recode_to_image_size(context, &mut maybe_sticker)
.await?;
@@ -2725,9 +2583,7 @@ async fn prepare_msg_common(
if let Some(reason) = chat.why_cant_send(context).await? {
if matches!(
reason,
CantSendReason::ProtectionBroken
| CantSendReason::ContactRequest
| CantSendReason::SecurejoinWait
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest
) && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification.
@@ -2737,18 +2593,6 @@ async fn prepare_msg_common(
}
}
// Check a quote reply is not leaking data from other chats.
// This is meant as a last line of defence, the UI should check that before as well.
// (We allow Chattype::Single in general for "Reply Privately";
// checking for exact contact_id will produce false positives when ppl just left the group)
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
if let Some(quoted_message) = msg.quoted_message(context).await? {
if quoted_message.chat_id != chat_id {
bail!("Bad quote reply");
}
}
}
// check current MessageState for drafts (to keep msg_id) ...
let update_msg_id = if msg.state == MessageState::OutDraft {
msg.hidden = false;
@@ -2908,7 +2752,7 @@ async fn prepare_send_msg(
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
let mimefactory = MimeFactory::from_msg(context, msg).await?;
let attach_selfavatar = mimefactory.attach_selfavatar;
let mut recipients = mimefactory.recipients();
@@ -2991,10 +2835,17 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
.await?;
}
if rendered_msg.last_added_location_id.is_some() {
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, last_added_location_id).await
{
error!(context, "Failed to set msg_location_id: {err:#}.");
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
@@ -3016,7 +2867,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.subject.clone_from(&rendered_msg.subject);
msg.update_subject(context).await?;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let chunk_size = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
for recipients_chunk in recipients.chunks(chunk_size) {
@@ -3781,10 +3636,7 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
if let Err(e) = send_msg(context, chat_id, &mut msg).await {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
return Err(e);
}
msg.id = send_msg(context, chat_id, &mut msg).await?;
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
@@ -3940,7 +3792,8 @@ pub async fn remove_contact_from_chat(
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
if contact_id == ContactId::SELF {
if contact.id == ContactId::SELF {
set_group_explicitly_left(context, &chat.grpid).await?;
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text = stock_str::msg_del_member_local(
@@ -3952,24 +3805,17 @@ pub async fn remove_contact_from_chat(
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
let res = send_msg(context, chat_id, &mut msg).await;
if contact_id == ContactId::SELF {
res?;
set_group_explicitly_left(context, &chat.grpid).await?;
} else if let Err(e) = res {
warn!(context, "remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}.");
}
msg.id = send_msg(context, chat_id, &mut msg).await?;
} else {
sync = Sync;
}
}
// we remove the member from the chat after constructing the
// to-be-send message. If between send_msg() and here the
// process dies, the user will be able to redo the action. It's better than the other
// way round: you removed someone from DB but no peer or device gets to know about it
// and group membership is thus different on different devices. But if send_msg()
// failed, we still remove the member locally, otherwise it would be impossible to
// remove a member with missing key from a protected group.
// process dies the user will have to re-do the action. It's
// better than the other way round: you removed
// someone from DB but no peer or device gets to know about it and
// group membership is thus different on different devices.
// Note also that sending a message needs all recipients
// in order to correctly determine encryption so if we
// removed it first, it would complicate the
@@ -4376,10 +4222,9 @@ pub async fn add_device_msg_with_importance(
timestamp_rcvd,
type,state,
txt,
txt_normalized,
param,
rfc724_mid)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?);",
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
(
chat_id,
ContactId::DEVICE,
@@ -4390,7 +4235,6 @@ pub async fn add_device_msg_with_importance(
msg.viewtype,
state,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
rfc724_mid,
),
@@ -4494,8 +4338,8 @@ pub(crate) async fn add_info_msg_with_cmd(
let row_id =
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
(
chat_id,
from_id.unwrap_or(ContactId::INFO),
@@ -4506,7 +4350,6 @@ pub(crate) async fn add_info_msg_with_cmd(
Viewtype::Text,
MessageState::InNoticed,
text,
message::normalize_text(text),
rfc724_mid,
ephemeral_timer,
param.to_string(),
@@ -4551,8 +4394,8 @@ pub(crate) async fn update_msg_text_and_timestamp(
context
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
(text, message::normalize_text(text), timestamp, msg_id),
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
(text, timestamp, msg_id),
)
.await?;
context.emit_msgs_changed(chat_id, msg_id);
@@ -4628,10 +4471,9 @@ impl Context {
}
_ => (),
}
ChatIdBlocked::lookup_by_contact(self, contact_id)
ChatId::lookup_by_contact(self, contact_id)
.await?
.with_context(|| format!("No chat for addr '{addr}'"))?
.id
}
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
@@ -4644,7 +4486,7 @@ impl Context {
.0
}
SyncId::Msgids(msgids) => {
let msg = message::get_by_rfc724_mids(self, msgids)
let msg = message::get_latest_by_rfc724_mids(self, msgids)
.await?
.with_context(|| format!("No message found for Message-IDs {msgids:?}"))?;
ChatId::lookup_by_message(&msg)
@@ -4878,59 +4720,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quote_replies() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?;
let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?;
let one2one_chat_id = alice.create_chat(&bob).await.id;
let one2one_msg_id = send_text_msg(&alice, one2one_chat_id, "foo".to_string()).await?;
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
// quoting messages in same chat is okay
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
let one2one_quote_reply_msg_id = result.unwrap();
// quoting messages from groups to one-to-ones is okay ("reply privately")
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_err());
// ... but forwarding messages with quotes is allowed
let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await;
assert!(result.is_ok());
// ... and bots are not restricted
alice.set_config(Config::Bot, Some("1")).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
@@ -5056,7 +4845,6 @@ mod tests {
// Bob leaves the chat.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
bob.pop_sent_msg().await;
// Bob receives a msg about Alice adding Claire to the group.
bob.recv_msg(&alice_sent_add_msg).await;
@@ -5109,7 +4897,6 @@ mod tests {
let sent_msg = alice.pop_sent_msg().await;
bob.recv_msg(&sent_msg).await;
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
bob.pop_sent_msg().await;
// This doesn't add Fiona back because Bob just removed them.
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
@@ -7563,27 +7350,4 @@ mod tests {
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
Ok(())
}
/// Tests sending JPEG image with .png extension.
///
/// This is a regression test, previously sending failed
/// because image was passed to PNG decoder
/// and it failed to decode image.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_jpeg_with_png_ext() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
let file = alice.get_blobdir().join("screenshot.png");
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let alice_chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let _msg = bob.recv_msg(&sent_msg).await;
Ok(())
}
}

View File

@@ -31,11 +31,10 @@ fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub fn str_to_color(s: &str) -> u32 {
pub(crate) fn str_to_color(s: &str) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{color:#08x}").replace("0x", "#")
}

View File

@@ -9,7 +9,7 @@ use base64::Engine as _;
use deltachat_contact_tools::addr_cmp;
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
@@ -254,9 +254,6 @@ pub enum Config {
/// True if account is configured.
Configured,
/// True if account is a chatmail account.
IsChatmail,
/// All secondary self addresses separated by spaces
/// (`addr1@example.org addr2@example.org addr3@example.org`)
SecondaryAddrs,
@@ -362,9 +359,6 @@ pub enum Config {
/// MsgId of webxdc map integration.
WebxdcIntegration,
/// Enable webxdc realtime features.
WebxdcRealtimeEnabled,
}
impl Config {
@@ -378,14 +372,14 @@ impl Config {
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
/// mustn't be controlled by other devices.
pub(crate) fn is_synced(&self) -> bool {
// NB: We don't restart IO from the synchronisation code, so `MvboxMove` isn't effective
// immediately if `ConfiguredMvboxFolder` is unset, but only after a reconnect (see
// `Imap::prepare()`).
// We don't restart IO from the synchronisation code, so this is to be on the safe side.
if self.needs_io_restart() {
return false;
}
matches!(
self,
Self::Displayname
| Self::MdnsEnabled
| Self::MvboxMove
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfstatus,
@@ -493,14 +487,6 @@ impl Context {
.is_some())
}
/// Returns true if sync messages should be sent.
///
/// This requires that both `SyncMsgs` and `BccSelf` settings are enabled.
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SyncMsgs).await?
&& self.get_config_bool(Config::BccSelf).await?)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -608,7 +594,7 @@ impl Context {
mut value: Option<&str>,
) -> Result<()> {
Self::check_config(key, value)?;
let sync = sync == Sync && key.is_synced() && self.is_configured().await?;
let sync = sync == Sync && key.is_synced();
let better_value;
match key {
@@ -981,12 +967,15 @@ mod tests {
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
for key in [Config::ShowEmails, Config::MvboxMove] {
let val = alice0.get_config_bool(key).await?;
alice0.set_config_bool(key, !val).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(key).await?, !val);
}
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
alice0
.set_config_bool(Config::ShowEmails, !show_emails)
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config_bool(Config::ShowEmails).await?,
!show_emails
);
// `Config::SyncMsgs` mustn't be synced.
alice0.set_config_bool(Config::SyncMsgs, false).await?;
@@ -1018,15 +1007,6 @@ mod tests {
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
sync(&alice0, &alice1).await;
// There was a bug that a sync message creates the self-chat with the user avatar instead of
// the special icon and that remains so when the self-chat becomes user-visible. Let's check
// this.
let self_chat = alice0.get_self_chat().await;
let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
assert_eq!(
self_chat_avatar_path,
alice0.get_blobdir().join("icon-saved-messages.png")
);
assert!(alice1
.get_config(Config::Selfavatar)
.await?

View File

@@ -112,11 +112,6 @@ impl Context {
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
// Reset our knowledge about whether the server is a chatmail server.
// We will update it when we connect to IMAP.
self.set_config_internal(Config::IsChatmail, None).await?;
let success = configure(self, &mut param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
@@ -458,15 +453,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
if imap_session.is_chatmail() {
ctx.set_config(Config::IsChatmail, Some("1")).await?;
ctx.set_config(Config::SentboxWatch, None).await?;
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
ctx.set_config(Config::ShowEmails, None).await?;
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
}
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
@@ -513,8 +499,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
/// Retrieve available autoconfigurations.
///
/// A. Search configurations from the domain used in the email-address
/// B. If we have no configuration yet, search configuration in Thunderbird's central database
/// A Search configurations from the domain used in the email-address, prefer encrypted
/// B. If we have no configuration yet, search configuration in Thunderbird's centeral database
async fn get_autoconfig(
ctx: &Context,
param: &LoginParam,
@@ -625,14 +611,14 @@ async fn try_imap_one_param(
match imap.connect(context).await {
Err(err) => {
info!(context, "IMAP failure: {err:#}.");
info!(context, "failure: {:#}", err);
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
}
Ok(session) => {
info!(context, "IMAP success: {inf}.");
info!(context, "success: {}", inf);
Ok((imap, session))
}
}
@@ -666,13 +652,13 @@ async fn try_smtp_one_param(
.connect(context, param, socks5_config, addr, provider_strict_tls)
.await
{
info!(context, "SMTP failure: {err:#}.");
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
} else {
info!(context, "SMTP success: {inf}.");
info!(context, "success: {}", inf);
smtp.disconnect();
Ok(())
}

View File

@@ -223,11 +223,6 @@ pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60;
/// in the group membership consistency algo to reject outdated membership changes.
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
/// How long a 1:1 chat can't be used for sending while the SecureJoin is in progress. This should
/// be 10-20 seconds so that we are reasonably sure that the app remains active and receiving also
/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;

View File

@@ -1,6 +1,6 @@
//! Contacts module
use std::cmp::{min, Reverse};
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::fmt;
use std::path::{Path, PathBuf};
@@ -8,11 +8,10 @@ use std::time::UNIX_EPOCH;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use base64::Engine as _;
pub use deltachat_contact_tools::may_be_valid_addr;
use deltachat_contact_tools::{
self as contact_tools, addr_cmp, addr_normalize, sanitize_name_and_addr, strip_rtlo_characters,
ContactAddress, VcardContact,
addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, strip_rtlo_characters,
ContactAddress,
};
use deltachat_derive::{FromSql, ToSql};
use rusqlite::OptionalExtension;
@@ -20,15 +19,14 @@ use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::aheader::EncryptPreference;
use crate::chat::{ChatId, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
use crate::key::{load_self_public_key, DcKey};
use crate::log::LogExt;
use crate::login_param::LoginParam;
use crate::message::MessageState;
@@ -37,9 +35,7 @@ use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, smeared_time, time, SystemTime,
};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -123,29 +119,6 @@ impl ContactId {
.await?;
Ok(())
}
/// Updates the origin of the contacts, but only if `origin` is higher than the current one.
pub(crate) async fn scaleup_origin(
context: &Context,
ids: &[Self],
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
&format!(
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
sql::repeat_vars(ids.len())
),
rusqlite::params_from_iter(
params_iter(&[origin])
.chain(params_iter(ids))
.chain(params_iter(&[origin])),
),
)
.await?;
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -186,162 +159,6 @@ impl rusqlite::types::FromSql for ContactId {
}
}
/// Returns a vCard containing contacts with the given ids.
pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<String> {
let now = time();
let mut vcard_contacts = Vec::with_capacity(contacts.len());
for id in contacts {
let c = Contact::get_by_id(context, *id).await?;
let key = match *id {
ContactId::SELF => Some(load_self_public_key(context).await?),
_ => Peerstate::from_addr(context, &c.addr)
.await?
.and_then(|peerstate| peerstate.take_key(false)),
};
let key = key.map(|k| k.to_base64());
let profile_image = match c.get_profile_image(context).await? {
None => None,
Some(path) => tokio::fs::read(path)
.await
.log_err(context)
.ok()
.map(|data| base64::engine::general_purpose::STANDARD.encode(data)),
};
vcard_contacts.push(VcardContact {
addr: c.addr,
authname: c.authname,
key,
profile_image,
// Use the current time to not reveal our or contact's online time.
timestamp: Ok(now),
});
}
Ok(contact_tools::make_vcard(&vcard_contacts))
}
/// Imports contacts from the given vCard.
///
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
/// regardless of whether they are just created, modified or left untouched.
pub async fn import_vcard(context: &Context, vcard: &str) -> Result<Vec<ContactId>> {
let contacts = contact_tools::parse_vcard(vcard);
let mut contact_ids = Vec::with_capacity(contacts.len());
for c in &contacts {
let Ok(id) = import_vcard_contact(context, c)
.await
.with_context(|| format!("import_vcard_contact() failed for {}", c.addr))
.log_err(context)
else {
continue;
};
contact_ids.push(id);
}
Ok(contact_ids)
}
async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Result<ContactId> {
let addr = ContactAddress::new(&contact.addr).context("Invalid address")?;
// Importing a vCard is also an explicit user action like creating a chat with the contact. We
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
// want `contact.authname` to be saved as the authname and not a locally given name.
let origin = Origin::CreateChat;
let (id, modified) =
match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
Ok(val) => val,
};
if modified != Modifier::None {
context.emit_event(EventType::ContactsChanged(Some(id)));
}
let key = contact.key.as_ref().and_then(|k| {
SignedPublicKey::from_base64(k)
.with_context(|| {
format!(
"import_vcard_contact: Cannot decode key for {}",
contact.addr
)
})
.log_err(context)
.ok()
});
if let Some(public_key) = key {
let timestamp = contact
.timestamp
.as_ref()
.map_or(0, |&t| min(t, smeared_time(context)));
let aheader = Aheader {
addr: contact.addr.clone(),
public_key,
prefer_encrypt: EncryptPreference::Mutual,
};
let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
Err(e) => {
warn!(
context,
"import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
);
return Ok(id);
}
Ok(p) => p,
};
let peerstate = if let Some(mut p) = peerstate {
p.apply_gossip(&aheader, timestamp);
p
} else {
Peerstate::from_gossip(&aheader, timestamp)
};
if let Err(e) = peerstate.save_to_db(&context.sql).await {
warn!(
context,
"import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
);
return Ok(id);
}
if let Err(e) = peerstate
.handle_fingerprint_change(context, timestamp)
.await
{
warn!(
context,
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
contact.addr
);
return Ok(id);
}
}
if modified != Modifier::Created {
return Ok(id);
}
let path = match &contact.profile_image {
Some(image) => match BlobObject::store_from_base64(context, image, "avatar").await {
Err(e) => {
warn!(
context,
"import_vcard_contact: Could not decode and save avatar for {}: {e:#}.",
contact.addr
);
None
}
Ok(path) => Some(path),
},
None => None,
};
if let Some(path) = path {
// Currently this value doesn't matter as we don't import the contact of self.
let was_encrypted = false;
if let Err(e) =
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
}
}
Ok(id)
}
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
@@ -547,10 +364,6 @@ impl Contact {
{
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.authname = context
.get_config(Config::Displayname)
.await?
.unwrap_or_default();
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
@@ -965,6 +778,7 @@ impl Contact {
for (name, addr) in split_address_book(addr_book) {
let (name, addr) = sanitize_name_and_addr(name, addr);
let name = normalize_name(&name);
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
@@ -1501,9 +1315,7 @@ impl Contact {
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
let contact_id = self.id;
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
{
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
} else {
// 1:1 chat does not exist.
@@ -1539,6 +1351,22 @@ impl Contact {
.await?;
Ok(exists)
}
/// Updates the origin of the contact, but only if new origin is higher than the current one.
pub async fn scaleup_origin_by_id(
context: &Context,
contact_id: ContactId,
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
(origin, contact_id, origin),
)
.await?;
Ok(())
}
}
pub(crate) async fn set_blocked(
@@ -1924,7 +1752,7 @@ impl RecentlySeenLoop {
#[cfg(test)]
mod tests {
use deltachat_contact_tools::{may_be_valid_addr, normalize_name};
use deltachat_contact_tools::may_be_valid_addr;
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
@@ -2987,157 +2815,4 @@ Until the false-positive is fixed:
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_make_n_import_vcard() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
bob.set_config(Config::Displayname, Some("Bob")).await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
tokio::fs::write(&avatar_path, avatar_bytes).await?;
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let chat = bob.create_chat(alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?;
let key_base64 = Peerstate::from_addr(alice, &bob_addr)
.await?
.unwrap()
.peek_key(false)
.unwrap()
.to_base64();
let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
assert_eq!(make_vcard(alice, &[]).await?, "".to_string());
let t0 = time();
let vcard = make_vcard(alice, &[bob_id, fiona_id]).await?;
let t1 = time();
// Just test that it's parsed as expected, `deltachat_contact_tools` crate has tests on the
// exact format.
let contacts = contact_tools::parse_vcard(&vcard);
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0].addr, bob_addr);
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
assert!(t0 <= timestamp && timestamp <= t1);
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
assert!(t0 <= timestamp && timestamp <= t1);
let alice = &TestContext::new_alice().await;
alice.evtracker.clear_events();
let contact_ids = import_vcard(alice, &vcard).await?;
assert_eq!(contact_ids.len(), 2);
for _ in 0..contact_ids.len() {
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_))))
.await;
}
let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?;
// This should be the same vCard except timestamps, check that roughly.
let contacts = contact_tools::parse_vcard(&vcard);
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0].addr, bob_addr);
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
assert!(contacts[0].timestamp.is_ok());
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?;
let sent_msg = alice.send_text(chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
// Bob only actually imports Fiona, though `ContactId::SELF` is also returned.
bob.evtracker.clear_events();
let contact_ids = import_vcard(bob, &vcard).await?;
bob.emit_event(EventType::Test);
assert_eq!(contact_ids.len(), 2);
assert_eq!(contact_ids[0], ContactId::SELF);
let ev = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
.await;
assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1])));
let ev = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test))
.await;
assert_eq!(ev, EventType::Test);
let vcard = make_vcard(bob, &[contact_ids[1]]).await?;
let contacts = contact_tools::parse_vcard(&vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "fiona@example.net");
assert_eq!(contacts[0].authname, "".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_ok());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_vcard_updates_only_key() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
bob.set_config(Config::Displayname, Some("Bob")).await?;
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
alice.evtracker.clear_events();
let alice_bob_id = import_vcard(alice, &vcard).await?[0];
let ev = alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
.await;
assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id)));
let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?;
let sent_msg = alice.send_text(chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
let bob = &TestContext::new().await;
bob.configure_addr(bob_addr).await;
bob.set_config(Config::Displayname, Some("Not Bob")).await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&avatar_path, avatar_bytes).await?;
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
SystemTime::shift(Duration::from_secs(1));
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
assert_eq!(alice_bob_contact.get_authname(), "Bob");
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
let msg = alice.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
stock_str::contact_setup_changed(alice, bob_addr).await
);
let sent_msg = alice.send_text(chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
// The old vCard is imported, but doesn't change Bob's key for Alice.
import_vcard(alice, &vcard).await?.first().unwrap();
let sent_msg = alice.send_text(chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
Ok(())
}
}

View File

@@ -12,7 +12,7 @@ use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
use tokio::sync::{Mutex, Notify, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
@@ -30,7 +30,6 @@ use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
@@ -289,9 +288,6 @@ pub struct InnerContext {
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: OnceCell<Iroh>,
}
/// The state of ongoing process.
@@ -449,7 +445,6 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: OnceCell::new(),
};
let ctx = Context {
@@ -466,10 +461,18 @@ impl Context {
return;
}
if self.is_chatmail().await.unwrap_or_default() {
let mut lock = self.ratelimit.write().await;
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
{
if self
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default()
.filter(|s| s.ends_with(".testrun.org"))
.is_some()
{
let mut lock = self.ratelimit.write().await;
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
}
self.scheduler.start(self.clone()).await;
}
@@ -487,34 +490,9 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(iroh) = self.iroh.get() {
iroh.network_change().await;
}
self.scheduler.maybe_network().await;
}
/// Returns true if an account is on a chatmail server.
pub async fn is_chatmail(&self) -> Result<bool> {
self.get_config_bool(Config::IsChatmail).await
}
/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => usize::MAX,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
Ok(val)
}
/// Does a background fetch
/// pauses the scheduler and does one imap fetch, then unpauses and returns
pub async fn background_fetch(&self) -> Result<()> {
@@ -547,7 +525,7 @@ impl Context {
.as_ref()
.filter(|quota| {
time_elapsed(&quota.modified)
< Duration::from_secs(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
> Duration::from_secs(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
})
.is_none()
};
@@ -821,8 +799,6 @@ impl Context {
res.insert("imap_server_id", format!("{server_id:?}"));
}
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
if let Some(metadata) = &*self.metadata.read().await {
if let Some(comment) = &metadata.comment {
res.insert("imap_server_comment", format!("{comment:?}"));
@@ -967,12 +943,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"webxdc_realtime_enabled",
self.get_config_bool(Config::WebxdcRealtimeEnabled)
.await?
.to_string(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
@@ -1259,12 +1229,12 @@ impl Context {
Ok(list)
}
/// Searches for messages containing the query string case-insensitively.
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
let real_query = query.trim().to_lowercase();
let real_query = query.trim();
if real_query.is_empty() {
return Ok(Vec::new());
}
@@ -1280,7 +1250,7 @@ impl Context {
WHERE m.chat_id=?
AND m.hidden=0
AND ct.blocked=0
AND IFNULL(txt_normalized, txt) LIKE ?
AND txt LIKE ?
ORDER BY m.timestamp,m.id;",
(chat_id, str_like_in_text),
|row| row.get::<_, MsgId>("id"),
@@ -1316,7 +1286,7 @@ impl Context {
AND m.hidden=0
AND c.blocked!=1
AND ct.blocked=0
AND IFNULL(txt_normalized, txt) LIKE ?
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
(str_like_in_text,),
|row| row.get::<_, MsgId>("id"),
@@ -1346,7 +1316,7 @@ impl Context {
Ok(sentbox.as_deref() == Some(folder_name))
}
/// Returns true if given folder name is the name of the "DeltaChat" folder.
/// Returns true if given folder name is the name of the "Delta Chat" folder.
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
@@ -1721,8 +1691,6 @@ mod tests {
msg2.set_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
alice.send_text(chat.id, "Δ-Chat").await;
// Global search with a part of text finds the message.
let res = alice.search_msgs(None, "ob").await?;
assert_eq!(res.len(), 1);
@@ -1735,12 +1703,6 @@ mod tests {
assert_eq!(res.first(), Some(&msg2.id));
assert_eq!(res.get(1), Some(&msg1.id));
// Search is case-insensitive.
for chat_id in [None, Some(chat.id)] {
let res = alice.search_msgs(chat_id, "δ-chat").await?;
assert_eq!(res.len(), 1);
}
// Global search with longer text does not find any message.
let res = alice.search_msgs(None, "foobarbaz").await?;
assert!(res.is_empty());

View File

@@ -57,7 +57,11 @@ pub(crate) async fn prepare_decryption(
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults { dkim_passed: false },
dkim_results: DkimResults {
dkim_passed: false,
dkim_should_work: false,
allow_keychange: true,
},
});
}
@@ -82,13 +86,15 @@ pub(crate) async fn prepare_decryption(
None
};
let dkim_results = handle_authres(context, mail, from).await?;
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
// Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange,
allow_aeap,
)
.await?;
@@ -281,15 +287,19 @@ pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<Signe
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// The param `allow_change` is used to prevent the autocrypt key from being changed
/// if we suspect that the message may be forged and have a spoofed sender identity.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
allow_change: bool,
allow_aeap: bool,
) -> Result<Option<Peerstate>> {
let allow_change = !context.is_self_addr(from).await?;
let allow_change = allow_change && !context.is_self_addr(from).await?;
let mut peerstate;
// Apply Autocrypt header

View File

@@ -184,7 +184,7 @@ impl Session {
bail!("Attempt to fetch UID 0");
}
self.select_with_uidvalidity(context, folder).await?;
self.select_folder(context, Some(folder)).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);

View File

@@ -74,7 +74,7 @@ use async_channel::Receiver;
use serde::{Deserialize, Serialize};
use tokio::time::timeout;
use crate::chat::{send_msg, ChatId, ChatIdBlocked};
use crate::chat::{send_msg, ChatId};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
use crate::contact::ContactId;
use crate::context::Context;
@@ -378,13 +378,11 @@ WHERE
.await?;
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.map(|c| c.id)
.unwrap_or_default();
let device_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE)
let device_chat_id = ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
.map(|c| c.id)
.unwrap_or_default();
let threshold_timestamp = now.saturating_sub(delete_device_after);
@@ -447,7 +445,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
for (msg_id, chat_id, viewtype, location_id) in rows {
transaction.execute(
"UPDATE msgs
SET chat_id=?, txt='', txt_normalized=NULL, subject='', txt_raw='',
SET chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE id=?",
(DC_CHAT_ID_TRASH, msg_id),
@@ -492,13 +490,11 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
/// `delete_device_after` setting being set.
async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<i64>> {
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.map(|c| c.id)
.unwrap_or_default();
let device_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE)
let device_chat_id = ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
.map(|c| c.id)
.unwrap_or_default();
let oldest_message_timestamp: Option<i64> = context
@@ -1024,7 +1020,7 @@ mod tests {
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?;
msg.id.trash(&t).await?;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
self_chat
@@ -1304,7 +1300,7 @@ mod tests {
let msg = alice.get_last_msg().await;
// Message is deleted when its timer expires.
msg.id.trash(&alice, false).await?;
msg.id.trash(&alice).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

View File

@@ -71,14 +71,7 @@ impl EventEmitter {
/// [`try_recv`]: Self::try_recv
pub async fn recv(&self) -> Option<Event> {
let mut lock = self.0.lock().await;
match lock.recv().await {
Err(async_broadcast::RecvError::Overflowed(n)) => Some(Event {
id: 0,
typ: EventType::EventChannelOverflow { n },
}),
Err(async_broadcast::RecvError::Closed) => None,
Ok(event) => Some(event),
}
lock.recv().await.ok()
}
/// Tries to receive an event without blocking.
@@ -93,19 +86,8 @@ impl EventEmitter {
// to avoid blocking
// in case there is a concurrent call to `recv`.
let mut lock = self.0.try_lock()?;
match lock.try_recv() {
Err(async_broadcast::TryRecvError::Overflowed(n)) => {
// Some events have been lost,
// but the channel is not closed.
Ok(Event {
id: 0,
typ: EventType::EventChannelOverflow { n },
})
}
res @ (Err(async_broadcast::TryRecvError::Empty)
| Err(async_broadcast::TryRecvError::Closed)
| Ok(_)) => Ok(res?),
}
let event = lock.try_recv()?;
Ok(event)
}
}

View File

@@ -279,15 +279,6 @@ pub enum EventType {
status_update_serial: StatusUpdateSerial,
},
/// Data received over an ephemeral peer channel.
WebxdcRealtimeData {
/// Message ID.
msg_id: MsgId,
/// Realtime data.
data: Vec<u8>,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.
@@ -311,14 +302,4 @@ pub enum EventType {
/// ID of the changed chat
chat_id: Option<ChatId>,
},
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow {
/// Number of events skipped.
n: u64,
},
}

View File

@@ -93,12 +93,6 @@ pub enum HeaderDef {
/// See <https://datatracker.ietf.org/doc/html/rfc8601>
AuthenticationResults,
/// Node address from iroh where direct addresses have been removed.
IrohNodeAddr,
/// Advertised gossip topic for one webxdc.
IrohGossipTopic,
#[cfg(test)]
TestHeader,
}

View File

@@ -22,7 +22,6 @@ use futures_lite::FutureExt;
use num_traits::FromPrimitive;
use rand::Rng;
use ratelimit::Ratelimit;
use url::Url;
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::chatlist_events;
@@ -112,8 +111,6 @@ pub(crate) struct ServerMetadata {
/// IMAP METADATA `/shared/admin` as defined in
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2>.
pub admin: Option<String>,
pub iroh_relay: Option<Url>,
}
impl async_imap::Authenticator for OAuth2 {
@@ -313,7 +310,7 @@ impl Imap {
if !ratelimit_duration.is_zero() {
warn!(
context,
"IMAP got rate limited, waiting for {} until can connect.",
"IMAP got rate limited, waiting for {} until can connect",
duration_to_str(ratelimit_duration),
);
let interrupted = async {
@@ -543,16 +540,15 @@ impl Imap {
return Ok(false);
}
session
let new_emails = session
.select_with_uidvalidity(context, folder)
.await
.with_context(|| format!("Failed to select folder {folder:?}"))?;
if !session.new_mail && !fetch_existing_msgs {
if !new_emails && !fetch_existing_msgs {
info!(context, "No new emails in folder {folder:?}.");
return Ok(false);
}
session.new_mail = false;
let uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
@@ -599,26 +595,20 @@ impl Imap {
// in the `INBOX.DeltaChat` folder again.
let _target;
let target = if let Some(message_id) = &message_id {
let msg_info =
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
let delete = if let Some((_, _, true)) = msg_info {
info!(context, "Deleting locally deleted message {message_id}.");
true
} else if let Some((_, ts_sent_old, _)) = msg_info {
let is_dup = if let Some((_, ts_sent_old)) =
message::rfc724_mid_exists(context, message_id).await?
{
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let ts_sent = headers
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
if is_dup {
info!(context, "Deleting duplicate message {message_id}.");
}
is_dup
is_dup_msg(is_chat_msg, ts_sent, ts_sent_old)
} else {
false
};
if delete {
if is_dup {
info!(context, "Deleting duplicate message {message_id}.");
&delete_target
} else if context
.sql
@@ -772,6 +762,10 @@ impl Imap {
context: &Context,
session: &mut Session,
) -> Result<()> {
if context.get_config_bool(Config::Bot).await? {
return Ok(()); // Bots don't want those messages
}
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
.await
.context("failed to get recipients from the sentbox")?;
@@ -841,7 +835,7 @@ impl Session {
// Collect pairs of UID and Message-ID.
let mut msgs = BTreeMap::new();
self.select_with_uidvalidity(context, folder).await?;
self.select_folder(context, Some(folder)).await?;
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
@@ -1042,7 +1036,7 @@ impl Session {
// MOVE/DELETE operations. This does not result in multiple SELECT commands
// being sent because `select_folder()` does nothing if the folder is already
// selected.
self.select_with_uidvalidity(context, folder).await?;
self.select_folder(context, Some(folder)).await?;
// Empty target folder name means messages should be deleted.
if target.is_empty() {
@@ -1090,7 +1084,7 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
self.select_with_uidvalidity(context, &folder)
self.select_folder(context, Some(&folder))
.await
.context("failed to select folder")?;
@@ -1134,7 +1128,7 @@ impl Session {
return Ok(());
}
self.select_with_uidvalidity(context, folder)
self.select_folder(context, Some(folder))
.await
.context("failed to select folder")?;
@@ -1455,16 +1449,11 @@ impl Session {
let mut comment = None;
let mut admin = None;
let mut iroh_relay = None;
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(
mailbox,
options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay)",
)
.get_metadata(mailbox, options, "(/shared/comment /shared/admin)")
.await?;
for m in metadata {
match m.entry.as_ref() {
@@ -1474,26 +1463,10 @@ impl Session {
"/shared/admin" => {
admin = m.value;
}
"/shared/vendor/deltachat/irohrelay" => {
if let Some(value) = m.value {
if let Ok(url) = Url::parse(&value) {
iroh_relay = Some(url);
} else {
warn!(
context,
"Got invalid URL from iroh relay metadata: {:?}.", value
);
}
}
}
_ => {}
}
}
*lock = Some(ServerMetadata {
comment,
admin,
iroh_relay,
});
*lock = Some(ServerMetadata { comment, admin });
Ok(())
}
@@ -1566,7 +1539,7 @@ impl Session {
) -> Result<Option<&'a str>> {
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
self.maybe_close_folder(context).await?;
self.select_folder(context, None).await?;
for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);

View File

@@ -32,14 +32,6 @@ pub(crate) struct Capabilities {
/// This is supported by <https://github.com/deltachat/chatmail>
pub can_push: bool,
/// True if the server has an XCHATMAIL capability
/// indicating that it is a <https://github.com/deltachat/chatmail> server.
///
/// This can be used to hide some advanced settings in the UI
/// that are only interesting for normal email accounts,
/// e.g. the ability to move messages to Delta Chat folder.
pub is_chatmail: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}

View File

@@ -61,7 +61,6 @@ async fn determine_capabilities(
can_condstore: caps.has_str("CONDSTORE"),
can_metadata: caps.has_str("METADATA"),
can_push: caps.has_str("XDELTAPUSH"),
is_chatmail: caps.has_str("XCHATMAIL"),
server_id,
};
Ok(capabilities)

View File

@@ -29,13 +29,9 @@ impl Session {
) -> Result<Self> {
use futures::future::FutureExt;
self.select_with_uidvalidity(context, folder).await?;
self.select_folder(context, Some(folder)).await?;
if self.server_sent_unsolicited_exists(context)? {
self.new_mail = true;
}
if self.new_mail {
return Ok(self);
}
@@ -96,9 +92,6 @@ impl Session {
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
self.inner = session;
// Fetch mail once we exit IDLE.
self.new_mail = true;
Ok(self)
}
}

View File

@@ -10,6 +10,12 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP Connection Lost or no connection established")]
ConnectionLost,
#[error("IMAP Folder name invalid: {0}")]
BadFolderName(String),
#[error("Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}")]
NoFolder(String, String),
@@ -27,8 +33,7 @@ impl ImapSession {
/// Issues a CLOSE command if selected folder needs expunge,
/// i.e. if Delta Chat marked a message there as deleted previously.
///
/// CLOSE is considerably faster than an EXPUNGE
/// because no EXPUNGE responses are sent, see
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
@@ -39,7 +44,6 @@ impl ImapSession {
info!(context, "close/expunge succeeded");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
}
}
Ok(())
@@ -48,12 +52,18 @@ impl ImapSession {
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
pub(crate) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
) -> Result<NewlySelected> {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
if let Some(folder) = folder {
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
}
}
@@ -61,26 +71,34 @@ impl ImapSession {
self.maybe_close_folder(context).await?;
// select new folder
let res = if self.can_condstore() {
self.select_condstore(folder).await
if let Some(folder) = folder {
let res = if self.can_condstore() {
self.select_condstore(folder).await
} else {
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
}
} else {
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
Ok(NewlySelected::No)
}
}
@@ -90,7 +108,7 @@ impl ImapSession {
context: &Context,
folder: &str,
) -> anyhow::Result<NewlySelected> {
match self.select_folder(context, folder).await {
match self.select_folder(context, Some(folder)).await {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
@@ -100,7 +118,7 @@ impl ImapSession {
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
// Need to recheck, could have been created in parallel.
}
let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
let select_res = self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"));
if select_res.is_err() {
create_res?;
}
@@ -116,14 +134,13 @@ impl ImapSession {
/// When selecting a folder for the first time, sets the uid_next to the current
/// mailbox.uid_next so that no old emails are fetched.
///
/// Updates `self.new_mail` if folder was previously unselected
/// and new mails are detected after selecting,
/// i.e. UIDNEXT advanced while the folder was closed.
/// Returns Result<new_emails> (i.e. whether new emails arrived),
/// if in doubt, returns new_emails=true so emails are fetched.
pub(crate) async fn select_with_uidvalidity(
&mut self,
context: &Context,
folder: &str,
) -> Result<()> {
) -> Result<bool> {
let newly_selected = self
.select_or_create_folder(context, folder)
.await
@@ -180,26 +197,28 @@ impl ImapSession {
mailbox.uid_next = new_uid_next;
if new_uid_validity == old_uid_validity {
if newly_selected == NewlySelected::Yes {
if let Some(new_uid_next) = new_uid_next {
if new_uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, new_uid_next).await?;
context.schedule_resync().await?;
}
// If UIDNEXT changed, there are new emails.
self.new_mail |= new_uid_next != old_uid_next;
} else {
warn!(context, "Folder {folder} was just selected but we failed to determine UIDNEXT, assume that it has new mail.");
self.new_mail = true;
let new_emails = if newly_selected == NewlySelected::No {
// The folder was not newly selected i.e. no SELECT command was run. This means that mailbox.uid_next
// was not updated and may contain an incorrect value. So, just return true so that
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
// new messages is only one command, just as a SELECT command)
true
} else if let Some(new_uid_next) = new_uid_next {
if new_uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, new_uid_next).await?;
context.schedule_resync().await?;
}
}
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
} else {
// We have no UIDNEXT and if in doubt, return true.
true
};
return Ok(());
return Ok(new_emails);
}
// UIDVALIDITY is modified, reset highest seen MODSEQ.
@@ -210,7 +229,6 @@ impl ImapSession {
let new_uid_next = new_uid_next.unwrap_or_default();
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
self.new_mail = true;
// Collect garbage entries in `imap` table.
context
@@ -233,7 +251,7 @@ impl ImapSession {
old_uid_next,
old_uid_validity,
);
Ok(())
Ok(false)
}
}

View File

@@ -40,11 +40,6 @@ pub(crate) struct Session {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
/// True if currently selected folder has new messages.
///
/// Should be false if no folder is currently selected.
pub new_mail: bool,
}
impl Deref for Session {
@@ -72,7 +67,6 @@ impl Session {
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
new_mail: false,
}
}
@@ -100,11 +94,6 @@ impl Session {
self.capabilities.can_push
}
// Returns true if IMAP server has `XCHATMAIL` capability.
pub fn is_chatmail(&self) -> bool {
self.capabilities.is_chatmail
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(&mut self) -> Result<Vec<async_imap::types::Name>> {
let list = self.list(Some(""), Some("*")).await?.try_collect().await?;

View File

@@ -38,7 +38,6 @@ use iroh::progress::ProgressEmitter;
use iroh::protocol::AuthToken;
use iroh::provider::{DataSource, Event, Provider, Ticket};
use iroh::Hash;
use iroh_old as iroh;
use tokio::fs::{self, File};
use tokio::io::{self, AsyncWriteExt, BufWriter};
use tokio::sync::broadcast::error::RecvError;

View File

@@ -94,7 +94,7 @@ pub mod webxdc;
#[macro_use]
mod dehtml;
mod authres;
pub mod color;
mod color;
pub mod html;
pub mod net;
pub mod plaintext;
@@ -106,7 +106,6 @@ pub mod receive_imf;
pub mod tools;
pub mod accounts;
pub mod peer_channels;
pub mod reaction;
/// If set IMAP/incoming and SMTP/outgoing MIME messages will be printed.

View File

@@ -483,21 +483,15 @@ pub async fn delete_all(context: &Context) -> Result<()> {
/// Only path locations are deleted.
/// POIs should be deleted when corresponding message is deleted.
pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
let Some(delete_device_after) = context.get_config_delete_device_after().await? else {
return Ok(());
};
let threshold_timestamp = now.saturating_sub(delete_device_after);
let deleted = context
.sql
.execute(
"DELETE FROM locations WHERE independent=0 AND timestamp < ?",
(threshold_timestamp,),
(now,),
)
.await?
> 0;
if deleted {
info!(context, "Deleted {deleted} expired locations.");
context.emit_location_changed(None).await?;
}
Ok(())
@@ -884,11 +878,8 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::message::MessageState;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::SystemTime;
use crate::test_utils::TestContext;
#[test]
fn test_kml_parse() {
@@ -1016,54 +1007,6 @@ Content-Disposition: attachment; filename="location.kml"
Ok(())
}
/// Tests that `location.kml` is not hidden and not seen if it contains a message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn receive_visible_location_kml() -> Result<()> {
let alice = TestContext::new_alice().await;
receive_imf(
&alice,
br#"Subject: locations
MIME-Version: 1.0
To: <alice@example.org>
From: <bob@example.net>
Date: Tue, 21 Dec 2021 00:00:00 +0000
Chat-Version: 1.0
Message-ID: <foobar@localhost>
Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Text message.
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
Content-Type: application/vnd.google-earth.kml+xml
Content-Disposition: attachment; filename="location.kml"
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document addr="bob@example.net">
<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
</Document>
</kml>
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
false,
)
.await?;
let received_msg = alice.get_last_msg().await;
assert_eq!(received_msg.text, "Text message.");
assert_eq!(received_msg.state, MessageState::InFresh);
let locations = get_range(&alice, None, None, 0, 0).await?;
assert_eq!(locations.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_locations_to_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -1086,8 +1029,6 @@ Content-Disposition: attachment; filename="location.kml"
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
let alice_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
assert_eq!(alice_msg.has_location(), false);
let msg = bob.recv_msg_opt(&sent).await.unwrap();
assert!(msg.chat_id == bob_chat_id);
@@ -1096,60 +1037,6 @@ Content-Disposition: attachment; filename="location.kml"
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.first().unwrap()).await?;
assert_eq!(bob_msg.chat_id, bob_chat_id);
assert_eq!(bob_msg.viewtype, Viewtype::Image);
assert_eq!(bob_msg.has_location(), false);
let bob_locations = get_range(&bob, None, None, 0, 0).await?;
assert_eq!(bob_locations.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_expired_locations() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Alice enables deletion of messages from device after 1 week.
alice
.set_config(Config::DeleteDeviceAfter, Some("604800"))
.await?;
// Bob enables deletion of messages from device after 1 day.
bob.set_config(Config::DeleteDeviceAfter, Some("86400"))
.await?;
let alice_chat = alice.create_chat(bob).await;
// Alice enables location streaming.
// Bob receives a message saying that Alice enabled location streaming.
send_locations_to_chat(alice, alice_chat.id, 60).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Alice gets new location from GPS.
assert_eq!(set(alice, 10.0, 20.0, 1.0).await?, true);
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
// 10 seconds later location sending stream manages to send location.
SystemTime::shift(Duration::from_secs(10));
delete_expired(alice, time()).await?;
maybe_send_locations(alice).await?;
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
// Day later Bob removes location.
SystemTime::shift(Duration::from_secs(86400));
delete_expired(alice, time()).await?;
delete_expired(bob, time()).await?;
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
// Week late Alice removes location.
SystemTime::shift(Duration::from_secs(604800));
delete_expired(alice, time()).await?;
delete_expired(bob, time()).await?;
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 0);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
Ok(())
}

View File

@@ -2,22 +2,20 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::str;
use anyhow::{ensure, format_err, Context as _, Result};
use deltachat_contact_tools::{parse_vcard, VcardContact};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use tokio::{fs, io};
use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId, ChatIdBlocked};
use crate::chat::{Chat, ChatId};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{self, Contact, ContactId};
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
use crate::download::DownloadState;
@@ -103,31 +101,23 @@ impl MsgId {
/// We keep some infos to
/// 1. not download the same message again
/// 2. be able to delete the message on the server if we want to
///
/// * `on_server`: Delete the message on the server also if it is seen on IMAP later, but only
/// if all parts of the message are trashed with this flag. `true` if the user explicitly
/// deletes the message. As for trashing a partially downloaded message when replacing it with
/// a fully downloaded one, see `receive_imf::add_parts()`.
pub async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
pub async fn trash(self, context: &Context) -> Result<()> {
let chat_id = DC_CHAT_ID_TRASH;
let deleted_subst = match on_server {
true => ", deleted=1",
false => "",
};
context
.sql
.execute(
// If you change which information is removed here, also change delete_expired_messages() and
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
&format!(
"UPDATE msgs SET \
chat_id=?, txt='', txt_normalized=NULL, \
subject='', txt_raw='', \
mime_headers='', \
from_id=0, to_id=0, \
param=''{deleted_subst} \
WHERE id=?"
),
r#"
UPDATE msgs
SET
chat_id=?, txt='',
subject='', txt_raw='',
mime_headers='',
from_id=0, to_id=0,
param=''
WHERE id=?;
"#,
(chat_id, self),
)
.await?;
@@ -617,20 +607,6 @@ impl Message {
self.param.get_path(Param::File, context).unwrap_or(None)
}
/// Returns vector of vcards if the file has a vCard attachment.
pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
if self.viewtype != Viewtype::Vcard {
return Ok(Vec::new());
}
let path = self
.get_file(context)
.context("vCard message does not have an attachment")?;
let bytes = tokio::fs::read(path).await?;
let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
Ok(parse_vcard(vcard_contents))
}
/// Save file copy at the user-provided path.
pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
let path_src = self.get_file(context).context("No file")?;
@@ -679,11 +655,9 @@ impl Message {
Ok(())
}
/// Check if a message has a POI location bound to it.
/// These locations are also returned by [`location::get_range()`].
/// The UI may decide to display a special icon beside such messages.
///
/// [`location::get_range()`]: crate::location::get_range
/// Check if a message has a location bound to it.
/// These messages are also returned by get_locations()
/// and the UI may decide to display a special icon beside such messages,
pub fn has_location(&self) -> bool {
self.location_id != 0
}
@@ -1090,30 +1064,6 @@ impl Message {
Ok(())
}
/// Makes message a vCard-containing message using the specified contacts.
pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
ensure!(
matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
"Wrong viewtype for vCard: {}",
self.viewtype,
);
let vcard = contact::make_vcard(context, contacts).await?;
self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
.await
}
/// Updates message state from the vCard attachment.
pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
let vcard = fs::read(path).await.context("Could not read {path}")?;
if let Some(summary) = get_vcard_summary(&vcard) {
self.param.set(Param::Summary1, summary);
} else {
warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
self.viewtype = Viewtype::File;
}
Ok(())
}
/// Set different sender name for a message.
/// This overrides the name set by the `set_config()`-option `displayname`.
pub fn set_override_sender_name(&mut self, name: Option<String>) {
@@ -1464,9 +1414,9 @@ pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
"tif" => (Viewtype::File, "image/tiff"),
"ttf" => (Viewtype::File, "font/ttf"),
"txt" => (Viewtype::File, "text/plain"),
"vcard" => (Viewtype::Vcard, "text/vcard"),
"vcf" => (Viewtype::Vcard, "text/vcard"),
"wav" => (Viewtype::Audio, "audio/wav"),
"vcard" => (Viewtype::File, "text/vcard"),
"vcf" => (Viewtype::File, "text/vcard"),
"wav" => (Viewtype::File, "audio/wav"),
"weba" => (Viewtype::File, "audio/webm"),
"webm" => (Viewtype::Video, "video/webm"),
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
@@ -1557,9 +1507,8 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await?;
}
let on_server = true;
msg_id
.trash(context, on_server)
.trash(context)
.await
.with_context(|| format!("Unable to trash message {msg_id}"))?;
@@ -1862,9 +1811,8 @@ pub async fn estimate_deletion_cnt(
from_server: bool,
seconds: i64,
) -> Result<usize> {
let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.map(|c| c.id)
.unwrap_or_default();
let threshold_timestamp = time() - seconds;
@@ -1903,26 +1851,23 @@ pub async fn estimate_deletion_cnt(
Ok(cnt)
}
/// See [`rfc724_mid_exists_ex()`].
/// See [`rfc724_mid_exists_and()`].
pub(crate) async fn rfc724_mid_exists(
context: &Context,
rfc724_mid: &str,
) -> Result<Option<(MsgId, i64)>> {
Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
.await?
.map(|(id, ts_sent, _)| (id, ts_sent)))
rfc724_mid_exists_and(context, rfc724_mid, "1").await
}
/// Returns [MsgId] and "sent" timestamp of the most recent message with given `rfc724_mid`
/// (Message-ID header) and bool `expr` result if such messages exists in the db.
/// Returns [MsgId] and "sent" timestamp of the message with given `rfc724_mid` (Message-ID header)
/// if it exists in the db.
///
/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
/// for all messages with the given `rfc724_mid`.
pub(crate) async fn rfc724_mid_exists_ex(
/// @param cond SQL subexpression for filtering messages.
pub(crate) async fn rfc724_mid_exists_and(
context: &Context,
rfc724_mid: &str,
expr: &str,
) -> Result<Option<(MsgId, i64, bool)>> {
cond: &str,
) -> Result<Option<(MsgId, i64)>> {
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
if rfc724_mid.is_empty() {
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
@@ -1932,15 +1877,13 @@ pub(crate) async fn rfc724_mid_exists_ex(
let res = context
.sql
.query_row_optional(
&("SELECT id, timestamp_sent, MIN(".to_string()
+ expr
+ ") FROM msgs WHERE rfc724_mid=? ORDER BY timestamp_sent DESC"),
&("SELECT id, timestamp_sent FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
(rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;
let timestamp_sent: i64 = row.get(1)?;
let expr_res: bool = row.get(2)?;
Ok((msg_id, timestamp_sent, expr_res))
Ok((msg_id, timestamp_sent))
},
)
.await?;
@@ -1948,43 +1891,21 @@ pub(crate) async fn rfc724_mid_exists_ex(
Ok(res)
}
/// Given a list of Message-IDs, returns the most relevant message found in the database.
/// Given a list of Message-IDs, returns the latest message found in the database.
///
/// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in
/// `mids`. This means Message-IDs should be ordered from the least late to the latest one (like in
/// the References header).
/// Only messages that are not in the trash chat are considered.
pub(crate) async fn get_by_rfc724_mids(
pub(crate) async fn get_latest_by_rfc724_mids(
context: &Context,
mids: &[String],
) -> Result<Option<Message>> {
let mut latest = None;
for id in mids.iter().rev() {
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
continue;
};
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
continue;
};
if msg.download_state == DownloadState::Done {
return Ok(Some(msg));
if let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? {
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
return Ok(Some(msg));
}
}
latest.get_or_insert(msg);
}
Ok(latest)
}
/// Returns the 1st part of summary text (i.e. before the dash if any) for a valid DeltaChat vCard.
pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
let vcard = str::from_utf8(vcard).ok()?;
let contacts = deltachat_contact_tools::parse_vcard(vcard);
let [c] = &contacts[..] else {
return None;
};
if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
return None;
}
Some(c.display_name().to_string())
Ok(None)
}
/// How a message is primarily displayed.
@@ -2014,8 +1935,7 @@ pub enum Viewtype {
Text = 10,
/// Image message.
/// If the image is a GIF and has the appropriate extension, the viewtype is auto-changed to
/// `Gif` when sending the message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
Image = 20,
@@ -2059,11 +1979,6 @@ pub enum Viewtype {
/// Message is an webxdc instance.
Webxdc = 80,
/// Message containing shared contacts represented as a vCard (virtual contact file)
/// with email addresses and possibly other fields.
/// Use `parse_vcard()` to retrieve them.
Vcard = 90,
}
impl Viewtype {
@@ -2081,20 +1996,10 @@ impl Viewtype {
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Webxdc => true,
Viewtype::Vcard => true,
}
}
}
/// Returns text for storing in the `msgs.txt_normalized` column (to make case-insensitive search
/// possible for non-ASCII messages).
pub(crate) fn normalize_text(text: &str) -> Option<String> {
if text.is_ascii() {
return None;
};
Some(text.to_lowercase()).filter(|t| t != text)
}
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -2362,7 +2267,6 @@ mod tests {
async fn test_set_override_sender_name() {
// send message with overridden sender name
let alice = TestContext::new_alice().await;
let alice2 = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat = alice.create_chat(&bob).await;
let contact_id = *chat::get_chat_contacts(&alice, chat.id)
@@ -2382,7 +2286,6 @@ mod tests {
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
assert_ne!(contact.get_display_name(), "over ride".to_string());
chat::send_msg(&alice, chat.id, &mut msg).await.unwrap();
let sent_msg = alice.pop_sent_msg().await;
// bob receives that message
let chat = bob.create_chat(&alice).await;
@@ -2392,7 +2295,7 @@ mod tests {
.first()
.unwrap();
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
let msg = bob.recv_msg(&sent_msg).await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.chat_id, chat.id);
assert_eq!(msg.text, "bla blubb");
assert_eq!(
@@ -2406,13 +2309,6 @@ mod tests {
// (mailing lists may also use `Sender:`-header)
let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
assert_ne!(chat.typ, Chattype::Mailinglist);
// Alice receives message on another device.
let msg = alice2.recv_msg(&sent_msg).await;
assert_eq!(
msg.get_override_sender_name(),
Some("over ride".to_string())
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2613,7 +2509,6 @@ mod tests {
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

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