mirror of
https://github.com/chatmail/core.git
synced 2026-10-04 12:10:25 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
974e32dd76 |
@@ -15,6 +15,6 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
+29
-15
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.97.1
|
||||
RUST_VERSION: 1.95.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -58,16 +58,30 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25
|
||||
- uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823
|
||||
with:
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
provider_database:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt
|
||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
runs-on: ubuntu-latest
|
||||
@@ -75,7 +89,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -114,7 +128,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -132,7 +146,7 @@ jobs:
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@3d7d7cd5ac7f994c1892ae0c06165095b9139094
|
||||
uses: taiki-e/install-action@60ae4ce63c7aeb6e96d7f572c1ec7fafbb17ca80
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -157,7 +171,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -186,7 +200,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -212,7 +226,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -238,7 +252,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -288,7 +302,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -300,7 +314,7 @@ jobs:
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -342,13 +356,13 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -78,11 +78,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -102,11 +102,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -153,11 +153,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -177,11 +177,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -204,11 +204,11 @@ jobs:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
@@ -397,11 +397,11 @@ jobs:
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7.0.0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -513,12 +513,15 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||
if: github.event_name == 'release'
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Check that current version ends with -dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
@@ -17,18 +17,20 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v7
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -79,11 +79,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
|
||||
@@ -14,11 +14,11 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
url: https://rs.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
url: https://py.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
url: https://c.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
|
||||
|
||||
build-ts:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -84,15 +84,15 @@ jobs:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
run: npm install
|
||||
- name: npm run build
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
url: https://cffi.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -125,4 +125,4 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
||||
-245
@@ -1,244 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.57.0] - 2026-07-25
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove heartbeat push notifications.
|
||||
- [**breaking**] remove provider-db handling and provider lookup APIs.
|
||||
- provider lookup APIs were removed from CFFI and JSON-RPC.
|
||||
|
||||
also removes offline provider database code and generated provider data,
|
||||
provider-specific fields in configure/transport paths, and REPL providerinfo.
|
||||
|
||||
### Documentation
|
||||
|
||||
- remove oauth2 from standards.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- accept messages from key contacts with forged From address.
|
||||
- enable TLS certificate compression.
|
||||
- read SMTP recipient limit from relay IMAP metadata.
|
||||
|
||||
### Fixes
|
||||
|
||||
- fixup CI failures.
|
||||
- never merge outer To headers if standard header protection is used.
|
||||
- Re-add oauth2 to serialized structs ([#8464](https://github.com/chatmail/core/pull/8464)).
|
||||
- migrate transports configured on 2.56 to also have a oauth:false flag.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.57.0-dev.
|
||||
- deps: bump actions/setup-node from 6 to 7.
|
||||
- deps: bump cachix/install-nix-action from 31.10.6 to 31.11.0.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.20 to 2.1.1.
|
||||
- deps: bump taiki-e/install-action from 2.82.10 to 2.83.4.
|
||||
- cargo: bump quinn-proto from 0.11.14 to 0.11.16.
|
||||
|
||||
## [2.56.0] - 2026-07-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove all oauth support and drop DC_LP_AUTH flags.
|
||||
- removed oauth2 module, dc_get_oauth2_url FFI function, DC_LP_AUTH flags and configured/serverflags, and the oauth2 parameter/field from SMTP/IMAP clients, JSON-RPC interfaces, and CLI tools.
|
||||
|
||||
also contains regenerated provider data after dropping oauth in the update script.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- do not set backup_time in exported databases.
|
||||
|
||||
### Fixes
|
||||
|
||||
- revert 207c2e6e4c1bec43204c3b8a46fcbbff67d54b3f because some users reported problems with it.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.56.0-dev.
|
||||
|
||||
## [2.55.0] - 2026-07-20
|
||||
|
||||
Minor release to fix CI because releasing 2.54.0 failed.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Node version to 24.
|
||||
|
||||
## [2.54.0] - 2026-07-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Deprecate `is_chatmail`.
|
||||
- UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`.
|
||||
- [**breaking**] `delete_transport()` must not be used by UIs anymore. Instead, `set_transport_unpublished()` must be called when a user clicks on "Remove".
|
||||
- [**breaking**] `list_transports()` doesn't return unpublished relays anymore.
|
||||
- UIs should use `list_transports()` rather than `list_transports_ex()`, because unpublished transports count as removed from the user point of view, and should not be shown in the relay list anymore.
|
||||
- deltachat-rpc-client: add `Account.set_transport_unpublished()`.
|
||||
- Add `MsgReadCountChanged` event.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Implement support for populating and maintaining a list of default relays ([#8341](https://github.com/chatmail/core/pull/8341)).
|
||||
- Remove hidden relays automatically ([#8402](https://github.com/chatmail/core/pull/8402)).
|
||||
- Automatically remove oldest unpublished relay in order to make space when the user wants to add more; don't allow more than 5 relays overall ([#8428](https://github.com/chatmail/core/pull/8428)).
|
||||
- Add silent group changes messages as InNoticed, not InSeen.
|
||||
- Remove `?emailaddress` argument from autoconfig URL that is not using a dedicated domain.
|
||||
- Remove `imap::Session::sync_seen_flags()` ([#7742](https://github.com/chatmail/core/pull/7742)).
|
||||
- Use CAPABILITY response code if IMAP LOGIN command returns it.
|
||||
- Increase max idle timeout for iroh backup receiver to 60 seconds.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Request MDNs for resent channel messages.
|
||||
- Make pre-messages w/o text want MDNs ([#8004](https://github.com/chatmail/core/pull/8004)).
|
||||
- Make truncated edited messages have HTML for receivers ([#8249](https://github.com/chatmail/core/pull/8249)).
|
||||
- Un-escape message footer marks in full messages (`get_html`) ([#8427](https://github.com/chatmail/core/pull/8427)).
|
||||
- Hide synced chat if we only know its visibility ([#8343](https://github.com/chatmail/core/pull/8343)).
|
||||
- Tombstone MDN before sending it ([#8252](https://github.com/chatmail/core/pull/8252)).
|
||||
- Recreate `imap_markseen` with `PRIMARY KEY` constraint.
|
||||
- Rerun the full securejoin protocol if the address was outdated ([#8358](https://github.com/chatmail/core/pull/8358)).
|
||||
- Return early from `receive_imf` to not tombstone Iroh-Node-Addr message if webxdc instance isn't found ([#8372](https://github.com/chatmail/core/pull/8372)).
|
||||
- Replace `last_added_location_id` with `last_added_location_timestamp`.
|
||||
- Do not put locations into pre-messages.
|
||||
- RUSTSEC-2026-0204 ([#8403](https://github.com/chatmail/core/pull/8403)).
|
||||
- Ensure public key signatures are not in the past compared to the public key.
|
||||
- Do not bubble up errors in IMAP candidate loop.
|
||||
- Do not log errors if full message is not available on any transport.
|
||||
- Apply reactions that arrived before the message at later time ([#8415](https://github.com/chatmail/core/pull/8415)).
|
||||
|
||||
### Performance
|
||||
|
||||
- Add timestamp to `msgs_index7` and speed up `Chatlist::try_load()` ([#7848](https://github.com/chatmail/core/pull/7848)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.97.1.
|
||||
- rrsync prepends the restricted upload path, we need to leave it out ([#8405](https://github.com/chatmail/core/pull/8405)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update STYLE.md: macros should be used only when necessary ([#8410](https://github.com/chatmail/core/pull/8410)).
|
||||
- `create_group_chat_unencrypted()` may lead to chat split on the first device.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Deprecate unused `SkipAutocrypt` param.
|
||||
- Remove commented out `RenderedEmail.envelope`.
|
||||
- Remove the ability to send messages with non-standard header protection.
|
||||
- Make `crate::pgp::symm_encrypt_message` non-async.
|
||||
- Move `ensure_secret_key_exists` into key.rs.
|
||||
- Improve comment ([#8366](https://github.com/chatmail/core/pull/8366)).
|
||||
- Remove `set_modseq()` function.
|
||||
- Remove unnecessary reference in format string.
|
||||
- Label the loop iterating over the candidates.
|
||||
- Remove `GROUP BY c.id` from chatlist queries.
|
||||
|
||||
### Tests
|
||||
|
||||
- securejoin: Check that "vc-{,request-}pubkey" messages don't contain displayname.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.54.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.81.1 to 2.81.8.
|
||||
- deps: bump taiki-e/install-action from 2.81.8 to 2.81.11.
|
||||
- update rPGP from 0.19.0 to 0.20.0.
|
||||
- update astral-tokio-tar from 0.6.2 to 0.6.3.
|
||||
- deps: bump anyhow to 1.0.103.
|
||||
- deps: bump actions/checkout from 6 to 7.
|
||||
- cargo: bump syn from 2.0.117 to 2.0.118.
|
||||
- cargo: bump quote from 1.0.45 to 1.0.46.
|
||||
- cargo: bump bytes from 1.11.1 to 1.12.0.
|
||||
- cargo: bump regex from 1.12.3 to 1.12.4.
|
||||
- cargo: bump log from 0.4.31 to 0.4.33.
|
||||
- cargo: bump hyper from 1.9.0 to 1.10.1.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7.
|
||||
- cargo: bump chrono from 0.4.44 to 0.4.45.
|
||||
- update quick-xml to 0.41.0.
|
||||
- cargo: bump brotli from 8.0.2 to 8.0.4.
|
||||
- cargo: bump smallvec from 1.15.1 to 1.15.2.
|
||||
- deps: bump taiki-e/install-action from 2.81.11 to 2.82.6.
|
||||
- update yanked spin@0.9.8 and spin@0.10.0.
|
||||
- deps: bump taiki-e/install-action from 2.82.6 to 2.82.10.
|
||||
- update async-imap to 0.11.3.
|
||||
|
||||
## [2.53.0] - 2026-06-15
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make quality of images sent in chats more consistent between images with different aspect ratio.
|
||||
- `MsgId::get_html`: Make only one db query.
|
||||
- Do not log the recipient list for sent messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not trash pre-messages without text but with a webxdc update.
|
||||
- Don't send or process webxdc status updates in pre-messages.
|
||||
- Ignore SecureJoin messages from blocked contacts ([#8295](https://github.com/chatmail/core/pull/8295)).
|
||||
- Do not abort IMAP connection if setting the push token fails.
|
||||
|
||||
### Documentation
|
||||
|
||||
- STYLE.md: Require to list columns explicitly in `INSERT` statements.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: switch to the "master" branch for naersk.
|
||||
- flake.nix: Use hostPlatform.rust.rustcTarget instead of hardcoding it.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump version to 2.52.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.79.10 to 2.81.1.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.19 to 2.0.20.
|
||||
- Bump version to 2.53.0-dev.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move the definition of the `target_wh`-variable.
|
||||
- Remove timesmearing.
|
||||
|
||||
### Tests
|
||||
|
||||
- Print multiline chat descriptions with debug formatter.
|
||||
- `exec_securejoin_qr_multi_device()`: Make inviter devices receive each other messages.
|
||||
- Fixup the tests after removing timesmearing.
|
||||
- Remove timeout from `pop_sent_msg_ex()`.
|
||||
|
||||
## [2.52.0] - 2026-06-09
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update the channel title after joining if the QR code included a wrong title ([#8260](https://github.com/chatmail/core/pull/8260)).
|
||||
- Don't send removal message to contact that hasn't been a chat member ([#8298](https://github.com/chatmail/core/pull/8298)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add cryptography-related statistics (`number_of_transports`, `key_version`, `key_algorithm`, `pubkey_size`, `number_of_keys`) ([#8293](https://github.com/chatmail/core/pull/8293), [#8297](https://github.com/chatmail/core/pull/8297)).
|
||||
- Add IMAP folder to `Context::get_info()` ([#8285](https://github.com/chatmail/core/pull/8285)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update preloaded DNS cache.
|
||||
- Use default aws-lc-rs cryptography provider for rustls.
|
||||
- Add exception for unmaintained proc-macro-error2 to deny.toml.
|
||||
- cargo: bump `pin-project` from 1.1.11 to 1.1.13.
|
||||
- cargo: bump `tokio` from 1.52.1 to 1.52.3.
|
||||
- cargo: bump `log` from 0.4.29 to 0.4.30.
|
||||
- cargo: bump `serde_json` from 1.0.149 to 1.0.150.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19.
|
||||
- deps: bump taiki-e/install-action from 2.79.2 to 2.79.10.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: fix windows cross-compilation by adding pthreads includes.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove support for building "source" packages for deltachat-rpc-server.
|
||||
|
||||
## [2.51.0] - 2026-05-29
|
||||
|
||||
### Features / Changes
|
||||
@@ -8536,9 +8297,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
||||
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
|
||||
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
||||
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
|
||||
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
|
||||
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
|
||||
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
|
||||
[2.56.0]: https://github.com/chatmail/core/compare/v2.55.0..v2.56.0
|
||||
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
|
||||
|
||||
Generated
+248
-271
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -78,10 +78,10 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.41", features = ["escape-html"] }
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
@@ -89,6 +89,7 @@ rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
@@ -100,9 +101,9 @@ tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.3", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6.2", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
|
||||
@@ -59,13 +59,6 @@ If column is already declared without `NOT NULL`, use `IFNULL` function to provi
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
List columns explicitly in `INSERT` statements:
|
||||
```
|
||||
INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0);
|
||||
```
|
||||
Otherwise if a new column with default value is added in a future DB version, an upgraded DB can't
|
||||
be used with the old code, e.g. after transferring a DB from a device running a newer version.
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
@@ -181,10 +174,3 @@ Calling `into()`, `try_into()` or `parse()`
|
||||
creates an indirection,
|
||||
which is hard to follow for people who are not familiar with Rust,
|
||||
or who are not using rust-analyzer.
|
||||
|
||||
## Use macros only when really needed
|
||||
|
||||
Macros can be hard to read for people unfamiliar with Rust,
|
||||
and can have surprising effects like evaluating arguments multiple times.
|
||||
Therefore, macros should only be used when really needed;
|
||||
using functions is usually better.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
@@ -29,16 +29,17 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use regex::regex;
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
|
||||
mod vcard;
|
||||
pub use vcard::{VcardContact, make_vcard, parse_vcard};
|
||||
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
@@ -87,7 +88,9 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
let (name, addr) = if let Some(captures) = regex!("(.*)<(.*)>").captures(addr.as_ref()) {
|
||||
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
captures.get(1).map_or("", |m| m.as_str())
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::regex;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::sanitize_name_and_addr;
|
||||
|
||||
@@ -208,7 +210,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
let unfolded_lines = regex!("\r?\n[\t ]").replace_all(vcard, "");
|
||||
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
@@ -220,10 +220,7 @@ END:VCARD
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_deref().unwrap(),
|
||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
|
||||
);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +244,7 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
@@ -278,15 +272,9 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
|
||||
);
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_ref().unwrap(),
|
||||
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
|
||||
);
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
+263
-40
@@ -19,6 +19,7 @@ typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_event_channel dc_event_channel_t;
|
||||
@@ -461,10 +462,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* This is deprecated, UIs should not behave differently
|
||||
* for chatmail relays and classical email servers.
|
||||
* Most usages in UIs can be replaced by `force_encryption`.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
@@ -502,6 +500,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `proxy_enabled` = Proxy enabled. Disabled by default.
|
||||
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
|
||||
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
@@ -588,7 +587,36 @@ int dc_set_config_from_qr (dc_context_t* context, const char* qr);
|
||||
char* dc_get_info (const dc_context_t* context);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get URL that can be used to initiate an OAuth2 authorization.
|
||||
*
|
||||
* If an OAuth2 authorization is possible for a given e-mail address,
|
||||
* this function returns the URL that should be opened in a browser.
|
||||
*
|
||||
* If the user authorizes access,
|
||||
* the given redirect_uri is called by the provider.
|
||||
* It's up to the UI to handle this call.
|
||||
*
|
||||
* The provider will attach some parameters to the URL,
|
||||
* most important the parameter `code` that should be set as the `mail_pw`.
|
||||
* With `server_flags` set to #DC_LP_AUTH_OAUTH2,
|
||||
* dc_configure() can be called as usual afterwards.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param addr E-mail address the user has entered.
|
||||
* In case the user selects a different e-mail address during
|
||||
* authorization, this is corrected in dc_configure()
|
||||
* @param redirect_uri URL that will get `code` that is used as `mail_pw` then.
|
||||
* Not all URLs are allowed here, however, the following should work:
|
||||
* `chat.delta:/PATH`, `http://localhost:PORT/PATH`,
|
||||
* `https://localhost:PORT/PATH`, `urn:ietf:wg:oauth:2.0:oob`
|
||||
* (the latter just displays the code the user can copy+paste then)
|
||||
* @return URL that can be opened in the browser to start OAuth2.
|
||||
* Returned strings must be released using dc_str_unref().
|
||||
* If OAuth2 is not possible for the given e-mail address, NULL is returned.
|
||||
*/
|
||||
char* dc_get_oauth2_url (dc_context_t* context, const char* addr, const char* redirect_uri);
|
||||
|
||||
|
||||
#define DC_CONNECTIVITY_NOT_CONNECTED 1000
|
||||
@@ -638,6 +666,24 @@ int dc_get_connectivity (dc_context_t* context);
|
||||
char* dc_get_connectivity_html (dc_context_t* context);
|
||||
|
||||
|
||||
#define DC_PUSH_NOT_CONNECTED 0
|
||||
#define DC_PUSH_HEARTBEAT 1
|
||||
#define DC_PUSH_CONNECTED 2
|
||||
|
||||
/**
|
||||
* Get the current push notification state.
|
||||
* One of:
|
||||
* - DC_PUSH_NOT_CONNECTED
|
||||
* - DC_PUSH_HEARTBEAT
|
||||
* - DC_PUSH_CONNECTED
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return Push notification state.
|
||||
*/
|
||||
int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
// connect
|
||||
|
||||
/**
|
||||
@@ -664,7 +710,7 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
* to get the full configuration from well-known URLs.
|
||||
*
|
||||
* - If _more_ options as `mail_server`, `mail_port`, `send_server`,
|
||||
* `send_port` or `send_user` are specified,
|
||||
* `send_port`, `send_user` or `server_flags` are specified,
|
||||
* **autoconfigure/autodiscover is skipped**.
|
||||
*
|
||||
* While dc_configure() returns immediately,
|
||||
@@ -872,14 +918,14 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
|
||||
|
||||
|
||||
/**
|
||||
* Check, if there is a single chat with a given contact.
|
||||
* Check, if there is a normal chat with a given contact.
|
||||
* To get the chat messages, use dc_get_chat_msgs().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param contact_id The contact ID to check.
|
||||
* @return If there is a single chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no single chat with the contact_id, the function
|
||||
* @return If there is a normal chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no normal chat with the contact_id, the function
|
||||
* returns 0.
|
||||
*/
|
||||
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
|
||||
@@ -1176,7 +1222,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to place a call for.
|
||||
* This needs to be a single chat.
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @param has_video Whether the call has video initially.
|
||||
@@ -1582,7 +1628,7 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
|
||||
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
|
||||
* and the user may create the chat again.
|
||||
* - **Groups are not left** - this would
|
||||
* be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
* be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
* from arriving, (2) leaving a group requires sending a message to
|
||||
* all group members - especially for groups not used for a longer time, this is
|
||||
* really unexpected when deletion results in contacting all members again,
|
||||
@@ -1600,7 +1646,7 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Block a chat.
|
||||
*
|
||||
* Blocking single chats blocks the corresponding contact. Blocking
|
||||
* Blocking 1:1 chats blocks the corresponding contact. Blocking
|
||||
* mailing lists creates a pseudo-contact in the list of blocked
|
||||
* contacts, so blocked mailing lists can be discovered and unblocked
|
||||
* the same way as the contacts. Blocking group chats deletes the
|
||||
@@ -1629,7 +1675,7 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Get the contact IDs belonging to a chat.
|
||||
*
|
||||
* - for single chats, the function always returns exactly one contact,
|
||||
* - for normal chats, the function always returns exactly one contact,
|
||||
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
*
|
||||
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -2067,7 +2113,7 @@ int dc_resend_msgs (dc_context_t* context, const uint3
|
||||
* The concrete action depends on the type of the chat and on the users settings
|
||||
* (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
*
|
||||
* - For single chats, the IMAP state is updated, MDN is sent
|
||||
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||
* (if dc_set_config()-options `mdns_enabled` is set)
|
||||
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
*
|
||||
@@ -3558,6 +3604,16 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
|
||||
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to get the associated context object.
|
||||
*
|
||||
* @memberof dc_chatlist_t
|
||||
* @param chatlist The chatlist object to empty.
|
||||
* @return The context object associated with the chatlist. NULL if none or on errors.
|
||||
*/
|
||||
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in JSON format.
|
||||
*
|
||||
@@ -3641,7 +3697,7 @@ char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get name of a chat. For single chats, this is the name of the contact.
|
||||
* Get name of a chat. For one-to-one chats, this is the name of the contact.
|
||||
* For group chats, this is the name given e.g. to dc_create_group_chat() or
|
||||
* received by a group-creation message.
|
||||
*
|
||||
@@ -3658,7 +3714,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
|
||||
* Get the chat's profile image.
|
||||
* For groups, this is the image set by any group member
|
||||
* using dc_set_chat_profile_image().
|
||||
* For single chats, this is the image set by each remote user on their own
|
||||
* For normal chats, this is the image set by each remote user on their own
|
||||
* using dc_set_config(context, "selfavatar", image).
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3672,7 +3728,7 @@ char* dc_chat_get_profile_image (const dc_chat_t* chat);
|
||||
|
||||
/**
|
||||
* Get a color for the chat.
|
||||
* For single chats, the color is calculated from the contact's e-mail address.
|
||||
* For 1:1 chats, the color is calculated from the contact's e-mail address.
|
||||
* Otherwise, the chat name is used.
|
||||
* The color can be used for an fallback avatar with white initials
|
||||
* as well as for headlines in bubbles of group chats.
|
||||
@@ -3739,7 +3795,7 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is a self talk. Self talks are single chats with
|
||||
* Check if a chat is a self talk. Self talks are normal chats with
|
||||
* the only contact DC_CONTACT_ID_SELF.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3794,9 +3850,9 @@ int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
/**
|
||||
* Check if the chat is encrypted.
|
||||
*
|
||||
* Single chats with key-contacts and group chats with key-contacts
|
||||
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||
* are encrypted.
|
||||
* Single chats with emails contacts and ad-hoc groups
|
||||
* 1:1 chats with emails contacts and ad-hoc groups
|
||||
* created for email threads are not encrypted.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -4330,16 +4386,16 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
|
||||
* display name, or NULL.
|
||||
*
|
||||
* If this returns non-NULL, put a `~` before the override-sender-name and show the
|
||||
* override-sender-name and the sender's avatar even in single chats.
|
||||
* override-sender-name and the sender's avatar even in 1:1 chats.
|
||||
*
|
||||
* In mailing lists, sender display name and sender address do not always belong together.
|
||||
* In this case, this function gives you the name that should actually be shown over the message.
|
||||
*
|
||||
* Also, sometimes, we need to indicate a different sender in single chats:
|
||||
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
|
||||
* Suppose that our user writes an e-mail to support@delta.chat, which forwards to
|
||||
* Bob <bob@delta.chat>, and Bob replies.
|
||||
*
|
||||
* Then, Bob's reply is shown in our single chat with support@delta.chat and the override-sender-name is
|
||||
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
|
||||
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
|
||||
* as in group messages. If the user then taps on the avatar, they can see that this message
|
||||
* comes from bob@delta.chat.
|
||||
@@ -5171,6 +5227,98 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_provider_t
|
||||
*
|
||||
* Opaque object containing information about one single e-mail provider.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* Lookup is done from the local database by extracting the domain from the e-mail address.
|
||||
* Therefore the provider for custom domains cannot be identified.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
* @param email The user's e-mail address to extract the provider info form.
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* DNS lookup is not used anymore and this function is deprecated.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
* @param email The user's e-mail address to extract the provider info form.
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
* URL of the overview page.
|
||||
*
|
||||
* This URL allows linking to the providers page on providers.delta.chat.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string with a fully-qualified URL,
|
||||
* if there is no such URL, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_overview_page (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Get hints to be shown to the user on the login screen.
|
||||
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
|
||||
* the UI may want to highlight the hint.
|
||||
*
|
||||
* Moreover, the UI should display a "More information" link
|
||||
* that forwards to the URL returned by dc_provider_get_overview_page().
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string with the hint to show to the user, may contain multiple lines,
|
||||
* if there is no such hint, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Whether DC works with this provider.
|
||||
*
|
||||
* Can be one of #DC_PROVIDER_STATUS_OK,
|
||||
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return The status as a constant number.
|
||||
*/
|
||||
int dc_provider_get_status (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Free the provider info struct.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
*/
|
||||
void dc_provider_unref (dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_lot_t
|
||||
*
|
||||
@@ -5484,7 +5632,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* A single chat with a single contact.
|
||||
* A one-to-one chat with a single contact.
|
||||
*
|
||||
* dc_get_chat_contacts() contains one record for the user.
|
||||
* DC_CONTACT_ID_SELF is added _only_ for a self talk.
|
||||
@@ -5585,6 +5733,41 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_LP DC_LP
|
||||
*
|
||||
* Flags for configuring IMAP and SMTP servers.
|
||||
* These flags are optional
|
||||
* and may be set together with the username, password etc.
|
||||
* via dc_set_config() using the key "server_flags".
|
||||
*
|
||||
* @addtogroup DC_LP
|
||||
* @{
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Force OAuth2 authorization. This flag does not skip automatic configuration.
|
||||
* Before calling dc_configure() with DC_LP_AUTH_OAUTH2 set,
|
||||
* the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||
*/
|
||||
#define DC_LP_AUTH_OAUTH2 0x2
|
||||
|
||||
|
||||
/**
|
||||
* Force NORMAL authorization, this is the default.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_AUTH_NORMAL 0x4
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#define DC_LP_AUTH_FLAGS (DC_LP_AUTH_OAUTH2|DC_LP_AUTH_NORMAL) // if none of these flags are set, the default is chosen
|
||||
|
||||
/**
|
||||
* @defgroup DC_CERTCK DC_CERTCK
|
||||
*
|
||||
@@ -6158,15 +6341,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELETED 2016
|
||||
|
||||
/**
|
||||
* Like @ref DC_EVENT_MSG_READ, but also fires on subsequent MDNs,
|
||||
* if there are multiple receivers, i.e. in groups and channels.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
#define DC_EVENT_MSG_READ_COUNT_CHANGED 2018
|
||||
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
@@ -6366,12 +6540,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
*/
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_TIMED_OUT 2201
|
||||
|
||||
/**
|
||||
* Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
*
|
||||
@@ -6497,6 +6665,61 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_MEDIA_QUALITY_WORSE 1
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||
*
|
||||
* These constants are used as return values for dc_provider_get_status().
|
||||
*
|
||||
* @addtogroup DC_PROVIDER_STATUS
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provider works out-of-the-box.
|
||||
* This provider status is returned for provider where the login
|
||||
* works by just entering the name or the e-mail address.
|
||||
*
|
||||
* - There is no need for the user to do any special things
|
||||
* (enable IMAP or so) in the provider's web interface or at other places.
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. are known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_OK 1
|
||||
|
||||
/**
|
||||
* Provider works, but there are preparations needed.
|
||||
*
|
||||
* - The user has to do some special things as "Enable IMAP in the web interface",
|
||||
* what exactly, is described in the string returned by dc_provider_get_before_login_hints()
|
||||
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. should be known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_PREPARATION 2
|
||||
|
||||
/**
|
||||
* Provider is not working.
|
||||
* This provider status is returned for providers
|
||||
* that are known to not work with Delta Chat.
|
||||
* The UI should block logging in with this provider.
|
||||
*
|
||||
* More information about that is typically provided
|
||||
* in the string returned by dc_provider_get_before_login_hints()
|
||||
* and in the page linked by dc_provider_get_overview_page().
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_BROKEN 3
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
|
||||
*
|
||||
@@ -6720,7 +6943,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message from %1$s"
|
||||
///
|
||||
/// Used in subjects of outgoing messages in single chats.
|
||||
/// Used in subjects of outgoing messages in one-to-one chats.
|
||||
/// - %1$s will be replaced by the name of the sender,
|
||||
/// this is the dc_set_config()-option `displayname` or `addr`
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
|
||||
+1153
-930
File diff suppressed because it is too large
Load Diff
+32
-44
@@ -17,15 +17,13 @@ use std::ptr;
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
@@ -166,40 +164,34 @@ pub(crate) trait Strdup {
|
||||
/// This function will panic when the original string contains an
|
||||
/// interior null byte as this can not be represented in raw C
|
||||
/// strings.
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl Strdup for str {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for String {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let s: &str = self;
|
||||
s.strdup()
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for std::path::Path {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for [u8] {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,15 +209,15 @@ pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [Strdup::strdup] for details.
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
unsafe { dc_strdup(tmp.as_ptr()) }
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -263,18 +255,20 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
unsafe {
|
||||
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
}
|
||||
}
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
unsafe { as_path_unicode(s) }
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
}
|
||||
|
||||
// Implementation for as_path() on Windows.
|
||||
@@ -282,7 +276,7 @@ pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path
|
||||
// Having this as a separate function means it can be tested on unix
|
||||
// too.
|
||||
#[allow(dead_code)]
|
||||
unsafe fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
|
||||
let cstr = unsafe { CStr::from_ptr(s) };
|
||||
@@ -370,20 +364,14 @@ mod tests {
|
||||
fn test_as_path() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(
|
||||
unsafe { as_path(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
)
|
||||
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_path_unicode_fn() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(
|
||||
unsafe { as_path_unicode(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
);
|
||||
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
|
||||
@@ -5,31 +5,31 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use deltachat::EventEmitter;
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
|
||||
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ex, markfresh_chat,
|
||||
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{Config, get_all_ui_config_keys};
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
|
||||
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::{
|
||||
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ex, get_existing_msg_ids,
|
||||
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
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::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
@@ -37,9 +37,10 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{Mutex, RwLock, watch};
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -53,6 +54,8 @@ use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::notify_state::JsonrpcNotifyState;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JsonrpcReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
@@ -64,7 +67,7 @@ use self::types::{
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
@@ -283,8 +286,12 @@ impl CommandApi {
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let lock = self.accounts.read().await;
|
||||
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds));
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
};
|
||||
// At this point account manager is not locked anymore.
|
||||
future.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -323,6 +330,12 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current push notification state.
|
||||
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.push_state().await.into())
|
||||
}
|
||||
|
||||
/// Get the combined filesize of an account in bytes
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -332,6 +345,21 @@ impl CommandApi {
|
||||
Ok(dbfile + total_size)
|
||||
}
|
||||
|
||||
/// Returns provider for the given domain.
|
||||
///
|
||||
/// This function looks up domain in offline database.
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
async fn get_provider_info(
|
||||
&self,
|
||||
_account_id: u32,
|
||||
email: String,
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
/// Checks if the context is already configured.
|
||||
async fn is_configured(&self, account_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -498,7 +526,7 @@ impl CommandApi {
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::set_transport_unpublished()] to remove a transport.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
@@ -524,31 +552,23 @@ impl CommandApi {
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::set_transport_unpublished()] to remove a transport.
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
/// Use [Self::list_transports_ex()] to additionally query
|
||||
/// whether the transports are marked as 'unpublished'.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|t| !t.is_unpublished)
|
||||
.map(|t| t.param.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Deprecated 2026-06: This is not needed by UI implementations anymore,
|
||||
/// because unpublished relays now count as removed from the user point of view,
|
||||
/// and must not be shown in the list of relays.
|
||||
/// This means that UIs should use `list_transports()` instead of this function.
|
||||
///
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
///
|
||||
/// As opposed to `list_transports()`, this function also returns unpublished transports,
|
||||
/// and for each returned transport it returns the information whether or not is `unpublished`.
|
||||
///
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::set_transport_unpublished()] to change whether a transport is 'published'.
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<TransportListEntry>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
@@ -560,31 +580,23 @@ impl CommandApi {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used by the automated tests.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
/// UIs should call this function when the user clicks on "Remove".
|
||||
/// Core will keep listening on this transport for some time,
|
||||
/// and automatically remove it once it is no longer needed.
|
||||
///
|
||||
/// Unpublished transports are not advertised to contacts,
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] are automatically removed.
|
||||
///
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`]: deltachat::sql::UNPUBLISHED_TRANSPORT_KEEP_TIME
|
||||
/// The default is false, but when the user updates from a version that didn't have this flag,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
async fn set_transport_unpublished(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -847,7 +859,7 @@ impl CommandApi {
|
||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
/// and the user may create the chat again.
|
||||
/// - **Groups are not left** - this would
|
||||
/// be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
/// from arriving, (2) leaving a group requires sending a message to
|
||||
/// all group members - especially for groups not used for a longer time, this is
|
||||
/// really unexpected when deletion results in contacting all members again,
|
||||
@@ -1012,7 +1024,7 @@ impl CommandApi {
|
||||
|
||||
/// Get the contact IDs belonging to a chat.
|
||||
///
|
||||
/// - for single chats, the function always returns exactly one contact,
|
||||
/// - for normal chats, the function always returns exactly one contact,
|
||||
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
///
|
||||
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -1069,9 +1081,7 @@ impl CommandApi {
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts. NB: Chats with similar names and the same members are merged on other
|
||||
/// devices, but usually users don't create such chats and look up the existing one instead, so
|
||||
/// chat split on the first device is acceptable.
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
@@ -1340,7 +1350,7 @@ impl CommandApi {
|
||||
/// The concrete action depends on the type of the chat and on the users settings
|
||||
/// (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
///
|
||||
/// - For single chats, the IMAP state is updated, MDN is sent
|
||||
/// - For normal chats, the IMAP state is updated, MDN is sent
|
||||
/// (if set_config()-options `mdns_enabled` is set)
|
||||
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
///
|
||||
@@ -1882,7 +1892,7 @@ impl CommandApi {
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns the [`ChatId`] for the single chat with `contact_id` if it exists.
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
async fn get_chat_id_by_contact_id(
|
||||
@@ -2267,7 +2277,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
let blob = message.get_webxdc_blob(&ctx, &path).await?;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{CallState, call_state};
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
|
||||
@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::{
|
||||
chat::{ChatVisibility, get_chat_contacts},
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
@@ -30,55 +30,37 @@ pub enum EventType {
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info {
|
||||
msg: String,
|
||||
},
|
||||
Info { msg: String },
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected {
|
||||
msg: String,
|
||||
},
|
||||
SmtpConnected { msg: String },
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected {
|
||||
msg: String,
|
||||
},
|
||||
ImapConnected { msg: String },
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent {
|
||||
msg: String,
|
||||
},
|
||||
SmtpMessageSent { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted {
|
||||
msg: String,
|
||||
},
|
||||
ImapMessageDeleted { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved {
|
||||
msg: String,
|
||||
},
|
||||
ImapMessageMoved { msg: String },
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile {
|
||||
file: String,
|
||||
},
|
||||
NewBlobFile { file: String },
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile {
|
||||
file: String,
|
||||
},
|
||||
DeletedBlobFile { file: String },
|
||||
|
||||
/// The library-user should write a warning string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Warning {
|
||||
msg: String,
|
||||
},
|
||||
Warning { msg: String },
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
@@ -90,18 +72,14 @@ pub enum EventType {
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a message box then.
|
||||
Error {
|
||||
msg: String,
|
||||
},
|
||||
Error { msg: String },
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// setChatName(), setChatProfileImage(),
|
||||
/// addContactToChat(), removeContactFromChat(),
|
||||
/// and messages sending functions.
|
||||
ErrorSelfNotInGroup {
|
||||
msg: String,
|
||||
},
|
||||
ErrorSelfNotInGroup { msg: String },
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
@@ -190,9 +168,7 @@ pub enum EventType {
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsNoticed {
|
||||
chat_id: u32,
|
||||
},
|
||||
MsgsNoticed { chat_id: u32 },
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
@@ -227,17 +203,6 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
|
||||
/// if there are multiple receivers, i.e. in groups and channels.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgReadCountChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message was deleted.
|
||||
///
|
||||
/// This event means that the message will no longer appear in the messagelist.
|
||||
@@ -267,9 +232,7 @@ pub enum EventType {
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatModified {
|
||||
chat_id: u32,
|
||||
},
|
||||
ChatModified { chat_id: u32 },
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -330,9 +293,7 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten {
|
||||
path: String,
|
||||
},
|
||||
ImexFileWritten { path: String },
|
||||
|
||||
/// Progress event sent when SecureJoin protocol has finished
|
||||
/// from the view of the inviter (Alice, the person who shows the QR code).
|
||||
@@ -510,9 +471,6 @@ pub enum EventType {
|
||||
/// synchronization messages arrives,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
TransportsModified,
|
||||
|
||||
// TODO documentation
|
||||
AccountsBackgroundFetchTimedOut,
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -588,10 +546,6 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgReadCountChanged { chat_id, msg_id } => MsgReadCountChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -706,8 +660,6 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::TransportsModified => TransportsModified,
|
||||
|
||||
CoreEventType::AccountsBackgroundFetchTimedOut => AccountsBackgroundFetchTimedOut,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct HttpResponse {
|
||||
|
||||
impl From<CoreHttpResponse> for HttpResponse {
|
||||
fn from(response: CoreHttpResponse) -> Self {
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||
let mimetype = response.mimetype;
|
||||
let encoding = response.encoding;
|
||||
|
||||
@@ -66,6 +66,10 @@ pub struct EnteredLoginParam {
|
||||
/// invalid hostnames.
|
||||
/// Default: Automatic
|
||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore).
|
||||
/// Default: false
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<dc::TransportListEntry> for TransportListEntry {
|
||||
@@ -96,6 +100,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
smtp_user: param.smtp.user.into_option(),
|
||||
smtp_password: param.smtp.password.into_option(),
|
||||
certificate_checks: certificate_checks.into_option(),
|
||||
oauth2: param.oauth2.into_option(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +127,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
password: param.smtp_password.unwrap_or_default(),
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: false,
|
||||
oauth2: param.oauth2.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,8 +174,9 @@ impl From<Socket> for dc::Socket {
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnteredCertificateChecks {
|
||||
/// `Automatic` means strict certificate checks,
|
||||
/// unless a legacy-domain override disables them.
|
||||
/// `Automatic` means that provider database setting should be taken.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// check certificates strictly.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
|
||||
@@ -390,11 +390,11 @@ pub enum SystemMessageType {
|
||||
LocationOnly,
|
||||
InvalidUnencryptedMail,
|
||||
|
||||
/// Single chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// to complete.
|
||||
SecurejoinWait,
|
||||
|
||||
/// Single chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// send messages.
|
||||
SecurejoinWaitTimeout,
|
||||
|
||||
@@ -726,9 +726,9 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
||||
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
||||
match value {
|
||||
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
||||
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
|
||||
duration: duration.get(),
|
||||
},
|
||||
deltachat::ephemeral::Timer::Enabled { duration } => {
|
||||
EphemeralTimer::Enabled { duration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod notify_state;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
pub mod webxdc;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use deltachat::push::NotifyState;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "NotifyState")]
|
||||
pub enum JsonrpcNotifyState {
|
||||
/// Not subscribed to push notifications.
|
||||
NotConnected,
|
||||
|
||||
/// Subscribed to heartbeat push notifications.
|
||||
Heartbeat,
|
||||
|
||||
/// Subscribed to push notifications for new messages.
|
||||
Connected,
|
||||
}
|
||||
|
||||
impl From<NotifyState> for JsonrpcNotifyState {
|
||||
fn from(state: NotifyState) -> Self {
|
||||
match state {
|
||||
NotifyState::NotConnected => Self::NotConnected,
|
||||
NotifyState::Heartbeat => Self::Heartbeat,
|
||||
NotifyState::Connected => Self::Connected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use deltachat::provider::Provider;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderInfo {
|
||||
/// Unique ID, corresponding to provider database filename.
|
||||
pub id: String,
|
||||
pub before_login_hint: String,
|
||||
pub overview_page: String,
|
||||
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
||||
}
|
||||
|
||||
impl ProviderInfo {
|
||||
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
||||
provider.map(|p| ProviderInfo {
|
||||
id: p.id.to_owned(),
|
||||
before_login_hint: p.before_login_hint.to_owned(),
|
||||
overview_page: p.overview_page.to_owned(),
|
||||
status: p.status.to_u32().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,6 @@ impl From<Qr> for QrObject {
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
@@ -256,7 +255,6 @@ impl From<Qr> for QrObject {
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
@@ -278,7 +276,6 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
..
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use deltachat::contact::ContactId;
|
||||
use deltachat::reaction::Reactions;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
@@ -33,24 +34,30 @@ pub struct JsonrpcReactions {
|
||||
impl From<Reactions> for JsonrpcReactions {
|
||||
fn from(reactions: Reactions) -> Self {
|
||||
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
||||
.by_contact
|
||||
.iter()
|
||||
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
||||
.collect();
|
||||
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||
|
||||
let reactions = reactions
|
||||
.frequencies
|
||||
.into_iter()
|
||||
.map(|entry| JsonrpcReaction {
|
||||
emoji: entry.reaction.as_str().to_string(),
|
||||
count: entry.count,
|
||||
is_from_self: entry.is_from_self,
|
||||
})
|
||||
.collect();
|
||||
let mut reactions_v = Vec::new();
|
||||
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||
let is_from_self = if let Some(self_reaction) = self_reaction {
|
||||
self_reaction.contains(&emoji)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let reaction = JsonrpcReaction {
|
||||
emoji,
|
||||
count,
|
||||
is_from_self,
|
||||
};
|
||||
reactions_v.push(reaction)
|
||||
}
|
||||
|
||||
JsonrpcReactions {
|
||||
reactions_by_contact,
|
||||
reactions,
|
||||
reactions: reactions_v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.58.0-dev"
|
||||
"version": "2.51.0-dev"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const constants = data
|
||||
key.startsWith("DC_CERTCK_") ||
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_") ||
|
||||
key.startsWith("DC_CHAT_TYPE")
|
||||
);
|
||||
|
||||
@@ -148,6 +148,23 @@ describe("online tests", function () {
|
||||
expect(message2.text).equal("super secret message");
|
||||
expect(message2.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
it("get provider info for example.com", async () => {
|
||||
const acc = await dc.rpc.addAccount();
|
||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
expect(info).to.be.not.null;
|
||||
expect(info?.overviewPage).to.equal(
|
||||
"https://providers.delta.chat/example-com",
|
||||
);
|
||||
expect(info?.status).to.equal(3);
|
||||
});
|
||||
|
||||
it("get provider info - domain and email should give same result", async () => {
|
||||
const acc = await dc.rpc.addAccount();
|
||||
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
|
||||
expect(info_email).to.deep.equal(info_domain);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
description = "Token bucket implementation"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,10 +5,9 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
@@ -25,6 +24,7 @@ use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use deltachat::{config, provider};
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
@@ -319,6 +319,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
info\n\
|
||||
set <configuration-key> [<value>]\n\
|
||||
get <configuration-key>\n\
|
||||
oauth2\n\
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
@@ -395,6 +396,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
createqrsvg <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
clear -- clear screen\n\
|
||||
@@ -1203,14 +1205,33 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
fs::write(&file, svg).await?;
|
||||
println!("{file:#?} written.");
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
match provider::get_provider_info(arg1) {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
println!("before_login_hint: {}", info.before_login_hint);
|
||||
println!("after_login_hint: {}", info.after_login_hint);
|
||||
println!("overview_page: {}", info.overview_page);
|
||||
for server in info.server.iter() {
|
||||
println!("server: {}:{}", server.hostname, server.port,);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("No information for provider belonging to {arg1} found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
|
||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
} else {
|
||||
bail!("Command failed.");
|
||||
};
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
}
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
|
||||
@@ -9,12 +9,14 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
|
||||
use anyhow::{Error, bail};
|
||||
use deltachat::EventType;
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -160,10 +162,11 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
"oauth2",
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
@@ -237,7 +240,7 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 13] = [
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
@@ -245,6 +248,7 @@ const MISC_COMMANDS: [&str; 13] = [
|
||||
"joinqr",
|
||||
"setqr",
|
||||
"createqrsvg",
|
||||
"providerinfo",
|
||||
"fileinfo",
|
||||
"estimatedeletion",
|
||||
"clear",
|
||||
@@ -266,11 +270,10 @@ impl Hinter for DcHelper {
|
||||
&CONTACT_COMMANDS[..],
|
||||
&MISC_COMMANDS[..],
|
||||
] {
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos]))
|
||||
&& *entry != line
|
||||
&& *entry != &line[..pos]
|
||||
{
|
||||
return Some(entry[pos..].to_owned());
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) {
|
||||
if *entry != line && *entry != &line[..pos] {
|
||||
return Some(entry[pos..].to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,6 +425,19 @@ async fn handle_cmd(
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
if let Some(oauth2_url) =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("OAuth2 not available for {addr}.");
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
}
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -133,10 +133,6 @@ class Account:
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
def set_transport_unpublished(self, addr: str, unpublished: bool = True):
|
||||
"""Unpublish the transport."""
|
||||
self._rpc.set_transport_unpublished(self.id, addr, unpublished)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
@@ -186,7 +182,7 @@ class Account:
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
"""Create a single chat with another account."""
|
||||
"""Create a 1:1 chat with another account."""
|
||||
return self.create_contact(account).create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
@@ -222,7 +218,7 @@ class Account:
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||
"""Return single chat for a contact if it exists."""
|
||||
"""Return 1:1 chat for a contact if it exists."""
|
||||
if isinstance(contact, Contact):
|
||||
assert contact.account == self
|
||||
contact_id = contact.id
|
||||
|
||||
@@ -252,7 +252,7 @@ class Chat:
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single chats self-address is not included.
|
||||
For single/direct chats self-address is not included.
|
||||
"""
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
@@ -54,7 +54,6 @@ class EventType(str, Enum):
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_READ_COUNT_CHANGED = "MsgReadCountChanged"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_DELETED = "ChatDeleted"
|
||||
@@ -97,7 +96,7 @@ class ChatType(str, Enum):
|
||||
"""Chat type."""
|
||||
|
||||
SINGLE = "Single"
|
||||
"""Single chat (a chat with a with a single contact)"""
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = "Group"
|
||||
|
||||
@@ -231,6 +230,14 @@ class KeyGenType(IntEnum):
|
||||
RSA4096 = 3
|
||||
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags."""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting."""
|
||||
|
||||
@@ -246,6 +253,14 @@ class ProviderStatus(IntEnum):
|
||||
BROKEN = 3
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state."""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
CONNECTED = 2
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode."""
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Contact:
|
||||
return snapshot
|
||||
|
||||
def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing single chat for this contact."""
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
|
||||
@@ -11,6 +11,8 @@ import subprocess
|
||||
import sys
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
@@ -102,7 +104,7 @@ class ACFactory:
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
"""Create a new single chat between ac1 and ac2 accepted on both sides.
|
||||
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||
|
||||
Returned chat is a chat with ac2 from ac1 point of view.
|
||||
"""
|
||||
@@ -174,9 +176,9 @@ def data():
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
for path in pathlib.Path(__file__).parents:
|
||||
datadir = path / "test-data"
|
||||
if datadir.is_dir():
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
self.path = datadir
|
||||
return
|
||||
raise Exception("Data path cannot be found")
|
||||
@@ -272,11 +274,10 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
The 'eval' function allows to remote-execute arbitrary expressions
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
from execnet import makegateway
|
||||
|
||||
def factory(core_version):
|
||||
python, rpc_server_path = get_core_python_env(core_version)
|
||||
gw = makegateway(f"popen//python={python}")
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
|
||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||
channel = gw.remote_exec(remote_bob_loop)
|
||||
|
||||
@@ -68,17 +68,13 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
msg.mark_seen()
|
||||
|
||||
rex = re.compile("Marked messages ([0-9,:]+) in folder INBOX as seen.")
|
||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# Each profile flags two messages but the logged UID set
|
||||
# covers a varying number of them, so just count UIDs mentioned.
|
||||
# We are not processing UID ranges, here we just care for two UIDs.
|
||||
for ac in ac1, ac2:
|
||||
uids = set()
|
||||
while len(uids) < 2:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
|
||||
uids.update(re.split("[,:]", match.group(1)))
|
||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||
break
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
|
||||
@@ -37,14 +37,19 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
log.section("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_chat(ac2)
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.get_snapshot().is_forwarded
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
assert self_addr not in ev.msg
|
||||
assert other_addr in ev.msg
|
||||
|
||||
log.section("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
@@ -52,16 +57,20 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only the second message, but not the first.
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.msg
|
||||
assert self_addr in ev.msg
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
|
||||
@@ -237,21 +237,10 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after unpublishing one works again
|
||||
account.set_transport_unpublished(second_addr)
|
||||
# test that adding a transport after deleting one works again
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# UIs are not expected to delete transports directly,
|
||||
# but we still test that adding a transport
|
||||
# after deleting one instead of unpublishing works.
|
||||
account.delete_transport(third_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
|
||||
@@ -487,7 +487,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
def test_qr_new_group_unblocked(acfactory):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked single chat with ac1 on ac2,
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
@@ -704,25 +704,3 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
|
||||
|
||||
def test_qr_scan_updates_new_relay_address(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_alice_chat = bob.secure_join(alice.get_qr_code())
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
for ac in [alice, bob]:
|
||||
old_addr = ac.get_config("configured_addr")
|
||||
ac.add_transport_from_qr(acfactory.get_account_qr())
|
||||
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
|
||||
ac.delete_transport(old_addr)
|
||||
|
||||
bob.secure_join(alice.get_qr_code())
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_alice_chat.send_text("hi")
|
||||
snapshot = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
|
||||
@@ -221,30 +221,6 @@ def test_account(acfactory) -> None:
|
||||
alice.stop_io()
|
||||
|
||||
|
||||
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_chat = alice_contact_bob.create_chat()
|
||||
alice_chat.send_text("Hello!")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
|
||||
bob_chat = bob.get_chat_by_id(chat_id)
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
bob_chat.accept()
|
||||
bob.mark_seen_messages([message])
|
||||
bob_chat.mark_fresh()
|
||||
assert bob_chat.get_fresh_message_count() == 1
|
||||
alice.wait_for_event(EventType.MSG_READ)
|
||||
alice_chat.send_text("You've read 'Hello!'")
|
||||
bob.wait_for_incoming_msg_event()
|
||||
assert bob_chat.get_fresh_message_count() == 2
|
||||
|
||||
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -275,7 +251,7 @@ def test_chat(acfactory) -> None:
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for single chats
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
@@ -380,7 +356,7 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
snapshot.text == "❌ Failed to receive a message:"
|
||||
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
||||
f" Core version {version}."
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||
)
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
@@ -474,35 +450,6 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
|
||||
chat_alice2 = alice2.create_chat(bob)
|
||||
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
|
||||
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_text("Hello!")
|
||||
msg_alice = alice.wait_for_incoming_msg()
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_bob_alice.send_text("What's new?")
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
|
||||
assert chat_alice2.get_fresh_message_count() == 2
|
||||
|
||||
msg_alice.mark_seen()
|
||||
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 1
|
||||
|
||||
msg_id = alice.wait_for_msgs_changed_event().msg_id
|
||||
msg = alice.get_message_by_id(msg_id)
|
||||
msg.mark_seen()
|
||||
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 0
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -747,6 +694,28 @@ def test_early_failure(tmp_path) -> None:
|
||||
rpc.start()
|
||||
|
||||
|
||||
def test_provider_info(rpc) -> None:
|
||||
account_id = rpc.add_account()
|
||||
|
||||
provider_info = rpc.get_provider_info(account_id, "example.org")
|
||||
assert provider_info["id"] == "example.com"
|
||||
|
||||
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
|
||||
assert provider_info is None
|
||||
|
||||
# Test MX record resolution.
|
||||
# This previously resulted in Gmail provider
|
||||
# because MX record pointed to google.com domain,
|
||||
# but MX record resolution has been removed.
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
|
||||
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1003,11 +972,6 @@ def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
|
||||
message.mark_seen()
|
||||
|
||||
# The MDN is queued in `smtp_mdns`, which is drained only after the regular
|
||||
# `smtp` queue, so "Outgoing message" would otherwise overtake it on the wire.
|
||||
# Wait for the read receipt to reach Alice before queueing "Outgoing message".
|
||||
alice.wait_for_event(EventType.MSG_READ)
|
||||
|
||||
# Send a message and wait until it arrives
|
||||
# in order to wait until Bob2 gets the markseen message.
|
||||
# This also tests that outgoing messages
|
||||
@@ -1135,7 +1099,6 @@ def test_rename_group(acfactory):
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
time.sleep(1)
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.58.0-dev"
|
||||
"version": "2.51.0-dev"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "deltachat-time"
|
||||
version = "1.0.0"
|
||||
description = "Time-related tools"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
edition = "2024"
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "3"
|
||||
syn = "2"
|
||||
quote = "1"
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let q = quote! {
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
@@ -24,7 +24,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
q.into()
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
@@ -32,7 +32,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let q = quote! {
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
@@ -44,5 +44,5 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
q.into()
|
||||
gen.into()
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ ignore = [
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
|
||||
# Unmaintained proc-macro-error2
|
||||
# Transitive dependency of typescript-type-def 0.5.13.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0173>
|
||||
"RUSTSEC-2026-0173",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
@@ -83,14 +78,11 @@ skip = [
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "snafu-derive", version = "0.8.5" },
|
||||
{ name = "snafu", version = "0.8.5" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "syn", version = "2.0.118" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
|
||||
Generated
+4
-3
@@ -94,15 +94,16 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780914171,
|
||||
"narHash": "sha256-NYoa+CvsCgayY356worC9g6QoZkKJmvrj6nVdEA2Lyk=",
|
||||
"lastModified": 1779912356,
|
||||
"narHash": "sha256-yj5O6vmAj+OfhTQMiUwhmQRP0HAII3BxEI6zuY6h/5k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "41b6e9efb62fac9c16d5617e0456a715928a2206",
|
||||
"rev": "33eaf5c72a67db15073322d26cd342c443556214",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "pull/391/head",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
naersk.url = "github:nix-community/naersk/pull/391/head";
|
||||
naersk.inputs.nixpkgs.follows = "nixpkgs";
|
||||
naersk.inputs.fenix.follows = "fenix";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
@@ -66,15 +66,37 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Map from architecture name to nixpkgs targets.
|
||||
# Map from architecture name to rust targets and nixpkgs targets.
|
||||
arch2targets = {
|
||||
"x86_64-linux" = "x86_64-unknown-linux-musl";
|
||||
"armv7l-linux" = "armv7l-unknown-linux-musleabihf";
|
||||
"armv6l-linux" = "armv6l-unknown-linux-musleabihf";
|
||||
"aarch64-linux" = "aarch64-unknown-linux-musl";
|
||||
"i686-linux" = "i686-unknown-linux-musl";
|
||||
"x86_64-darwin" = "x86_64-darwin";
|
||||
"aarch64-darwin" = "aarch64-darwin";
|
||||
"x86_64-linux" = {
|
||||
rustTarget = "x86_64-unknown-linux-musl";
|
||||
crossTarget = "x86_64-unknown-linux-musl";
|
||||
};
|
||||
"armv7l-linux" = {
|
||||
rustTarget = "armv7-unknown-linux-musleabihf";
|
||||
crossTarget = "armv7l-unknown-linux-musleabihf";
|
||||
};
|
||||
"armv6l-linux" = {
|
||||
rustTarget = "arm-unknown-linux-musleabihf";
|
||||
crossTarget = "armv6l-unknown-linux-musleabihf";
|
||||
};
|
||||
"aarch64-linux" = {
|
||||
rustTarget = "aarch64-unknown-linux-musl";
|
||||
crossTarget = "aarch64-unknown-linux-musl";
|
||||
};
|
||||
"i686-linux" = {
|
||||
rustTarget = "i686-unknown-linux-musl";
|
||||
crossTarget = "i686-unknown-linux-musl";
|
||||
};
|
||||
|
||||
"x86_64-darwin" = {
|
||||
rustTarget = "x86_64-apple-darwin";
|
||||
crossTarget = "x86_64-darwin";
|
||||
};
|
||||
"aarch64-darwin" = {
|
||||
rustTarget = "aarch64-apple-darwin";
|
||||
crossTarget = "aarch64-darwin";
|
||||
};
|
||||
};
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
@@ -91,10 +113,10 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -125,7 +147,6 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
@@ -137,10 +158,12 @@
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "i686-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -180,7 +203,6 @@
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
@@ -193,7 +215,6 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
@@ -207,12 +228,14 @@
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
rustTarget = arch2targets."${arch}".rustTarget;
|
||||
crossTarget = arch2targets."${arch}".crossTarget;
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
in
|
||||
let
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "format-flowed"
|
||||
version = "1.0.0"
|
||||
description = "format=flowed support"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["email"]
|
||||
|
||||
@@ -78,11 +78,11 @@ pub fn format_flowed(text: &str) -> String {
|
||||
|
||||
let mut prefix = prefix.to_string();
|
||||
|
||||
if quote_depth > 0
|
||||
&& let Some(s) = line.strip_prefix(' ')
|
||||
{
|
||||
line = s;
|
||||
prefix += " ";
|
||||
if quote_depth > 0 {
|
||||
if let Some(s) = line.strip_prefix(' ') {
|
||||
line = s;
|
||||
prefix += " ";
|
||||
}
|
||||
}
|
||||
|
||||
result += &format_line_flowed(line, &prefix);
|
||||
@@ -224,7 +224,8 @@ mod tests {
|
||||
fn test_unformat_flowed() {
|
||||
let text = "this is a very long message that should be wrapped using format=flowed and \n\
|
||||
unwrapped on the receiver";
|
||||
let expected = "this is a very long message that should be wrapped using format=flowed and \
|
||||
let expected =
|
||||
"this is a very long message that should be wrapped using format=flowed and \
|
||||
unwrapped on the receiver";
|
||||
assert_eq!(unformat_flowed(text, false), expected);
|
||||
|
||||
@@ -254,7 +255,8 @@ mod tests {
|
||||
assert_eq!(format_flowed_quote(quote), expected);
|
||||
|
||||
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
|
||||
let expected = "> this is a very long quote that should be wrapped using format=flowed and \r\n\
|
||||
let expected =
|
||||
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
|
||||
> unwrapped on the receiver";
|
||||
assert_eq!(format_flowed_quote(quote), expected);
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
name = "deltachat-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.58.0-dev"
|
||||
version = "2.51.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -397,7 +397,7 @@ class Account:
|
||||
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a single chat with Account, Contact or e-mail address."""
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def create_group_chat(
|
||||
|
||||
@@ -74,7 +74,7 @@ class Chat:
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_single(self) -> bool:
|
||||
"""Return True if this chat is a single chat, False otherwise."""
|
||||
"""Return True if this chat is a single/direct chat, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
|
||||
|
||||
def is_mailinglist(self) -> bool:
|
||||
@@ -458,7 +458,7 @@ class Chat:
|
||||
"""Get group profile image.
|
||||
|
||||
For groups, this is the image set by any group member using
|
||||
set_chat_profile_image(). For single chats, this is the image
|
||||
set_chat_profile_image(). For normal chats, this is the image
|
||||
set by each remote user on their own using dc_set_config(context,
|
||||
"selfavatar", image).
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
|
||||
@@ -107,7 +107,7 @@ class Contact:
|
||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
||||
|
||||
def create_chat(self):
|
||||
"""create or get an existing single chat object for the specified contact or contact id.
|
||||
"""create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
:param contact: chat_id (int) or contact object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
|
||||
@@ -226,10 +226,6 @@ class EventThread(threading.Thread):
|
||||
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.event_emitter = ffi.gc(
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.daemon = True
|
||||
self._marked_for_shutdown = False
|
||||
@@ -254,9 +250,13 @@ class EventThread(threading.Thread):
|
||||
def run(self) -> None:
|
||||
"""get and run events until shutdown."""
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
event_emitter = ffi.gc(
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
with self.swallow_and_log_exception("Unexpected error in event thread"):
|
||||
event = lib.dc_get_next_event(self.event_emitter)
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL or self._marked_for_shutdown:
|
||||
break
|
||||
self._process_event(event)
|
||||
|
||||
@@ -283,7 +283,7 @@ class Message:
|
||||
lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name))
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the single chat with the sender of this message.
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
|
||||
:returns: :class:`deltachat.chat.Chat` instance
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Provider info class."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer
|
||||
|
||||
|
||||
class ProviderNotFoundError(Exception):
|
||||
"""The provider information was not found."""
|
||||
|
||||
|
||||
class Provider:
|
||||
"""
|
||||
Provider information.
|
||||
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
)
|
||||
if provider == ffi.NULL:
|
||||
raise ProviderNotFoundError("Provider not found")
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_OK`,
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_PREPARATION` or
|
||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_BROKEN` constants.
|
||||
"""
|
||||
return lib.dc_provider_get_status(self._provider)
|
||||
@@ -171,6 +171,14 @@ def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
assert msg.id == 0
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_open(tmp_path):
|
||||
db_fname = tmp_path / "test.db"
|
||||
ctx = ffi.gc(
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-07-25
|
||||
2026-05-29
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
# if the yaml import fails, run "pip install pyyaml"
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
out_all = ""
|
||||
out_domains = ""
|
||||
out_ids = ""
|
||||
domains_set = set()
|
||||
|
||||
|
||||
def camel(name):
|
||||
words = name.split("_")
|
||||
return "".join(w.capitalize() for i, w in enumerate(words))
|
||||
|
||||
|
||||
def cleanstr(s):
|
||||
s = s.strip()
|
||||
s = s.replace("\n", " ")
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace('"', '\\"')
|
||||
return s
|
||||
|
||||
|
||||
def file2id(f):
|
||||
return f.stem
|
||||
|
||||
|
||||
def file2varname(f):
|
||||
f = file2id(f)
|
||||
f = f.replace(".", "_")
|
||||
f = f.replace("-", "_")
|
||||
return "P_" + f.upper()
|
||||
|
||||
|
||||
def file2url(f):
|
||||
f = file2id(f)
|
||||
f = f.replace(".", "-")
|
||||
return "https://providers.delta.chat/" + f
|
||||
|
||||
|
||||
def process_opt(data):
|
||||
if not "opt" in data:
|
||||
return "ProviderOptions::new()"
|
||||
opt = "ProviderOptions {\n"
|
||||
opt_data = data.get("opt", "")
|
||||
for key in opt_data:
|
||||
value = str(opt_data[key])
|
||||
if key == "max_smtp_rcpt_to":
|
||||
value = "Some(" + value + ")"
|
||||
if value in {"True", "False"}:
|
||||
value = value.lower()
|
||||
opt += " " + key + ": " + value + ",\n"
|
||||
opt += " ..ProviderOptions::new()\n"
|
||||
opt += " }"
|
||||
return opt
|
||||
|
||||
|
||||
def process_config_defaults(data):
|
||||
if not "config_defaults" in data:
|
||||
return "None"
|
||||
defaults = "Some(&[\n"
|
||||
config_defaults = data.get("config_defaults", "")
|
||||
for key in config_defaults:
|
||||
value = str(config_defaults[key])
|
||||
defaults += (
|
||||
" ConfigDefault { key: Config::"
|
||||
+ camel(key)
|
||||
+ ', value: "'
|
||||
+ value
|
||||
+ '" },\n'
|
||||
)
|
||||
defaults += " ])"
|
||||
return defaults
|
||||
|
||||
|
||||
def process_data(data, file):
|
||||
status = data.get("status", "")
|
||||
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
|
||||
raise TypeError("bad status")
|
||||
|
||||
comment = ""
|
||||
domains = ""
|
||||
if not "domains" in data:
|
||||
raise TypeError("no domains found")
|
||||
for domain in data["domains"]:
|
||||
domain = cleanstr(domain)
|
||||
if domain == "" or domain.lower() != domain:
|
||||
raise TypeError("bad domain: " + domain)
|
||||
|
||||
global domains_set
|
||||
if domain in domains_set:
|
||||
raise TypeError("domain used twice: " + domain)
|
||||
domains_set.add(domain)
|
||||
|
||||
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
|
||||
comment += domain + ", "
|
||||
|
||||
ids = ""
|
||||
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
|
||||
|
||||
server = ""
|
||||
has_imap = False
|
||||
has_smtp = False
|
||||
if "server" in data:
|
||||
for s in data["server"]:
|
||||
hostname = cleanstr(s.get("hostname", ""))
|
||||
port = int(s.get("port", ""))
|
||||
if hostname == "" or hostname.lower() != hostname or port <= 0:
|
||||
raise TypeError("bad hostname or port")
|
||||
|
||||
protocol = s.get("type", "").upper()
|
||||
if protocol == "IMAP":
|
||||
has_imap = True
|
||||
elif protocol == "SMTP":
|
||||
has_smtp = True
|
||||
else:
|
||||
raise TypeError("bad protocol")
|
||||
|
||||
socket = s.get("socket", "").upper()
|
||||
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
|
||||
raise TypeError("bad socket")
|
||||
|
||||
username_pattern = s.get("username_pattern", "EMAIL").upper()
|
||||
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
|
||||
raise TypeError("bad username pattern")
|
||||
|
||||
server += (
|
||||
" Server { protocol: "
|
||||
+ protocol.capitalize()
|
||||
+ ", socket: "
|
||||
+ socket.capitalize()
|
||||
+ ', hostname: "'
|
||||
+ hostname
|
||||
+ '", port: '
|
||||
+ str(port)
|
||||
+ ", username_pattern: "
|
||||
+ username_pattern.capitalize()
|
||||
+ " },\n"
|
||||
)
|
||||
|
||||
opt = process_opt(data)
|
||||
config_defaults = process_config_defaults(data)
|
||||
|
||||
oauth2 = data.get("oauth2", "")
|
||||
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += (
|
||||
"static "
|
||||
+ file2varname(file)
|
||||
+ ": Provider = Provider {\n"
|
||||
)
|
||||
provider += ' id: "' + file2id(file) + '",\n'
|
||||
provider += " status: Status::" + status.capitalize() + ",\n"
|
||||
provider += ' before_login_hint: "' + before_login_hint + '",\n'
|
||||
provider += ' after_login_hint: "' + after_login_hint + '",\n'
|
||||
provider += ' overview_page: "' + file2url(file) + '",\n'
|
||||
provider += " server: &[\n" + server + " ],\n"
|
||||
provider += " opt: " + opt + ",\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += "};\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
if status != "OK" and before_login_hint == "":
|
||||
raise TypeError(
|
||||
"status PREPARATION or BROKEN requires before_login_hint: " + file
|
||||
)
|
||||
|
||||
# finally, add the provider
|
||||
global out_all, out_domains, out_ids
|
||||
out_all += "// " + file.name + ": " + comment.strip(", ") + "\n"
|
||||
|
||||
# also add provider with no special things to do -
|
||||
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
|
||||
out_all += provider
|
||||
out_domains += domains
|
||||
out_ids += ids
|
||||
|
||||
|
||||
def process_file(file):
|
||||
print("processing file: {}".format(file), file=sys.stderr)
|
||||
with open(file) as f:
|
||||
# load_all() loads "---"-separated yamls -
|
||||
# by coincidence, this is also the frontmatter separator :)
|
||||
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
|
||||
process_data(data, file)
|
||||
|
||||
|
||||
def process_dir(dir):
|
||||
print("processing directory: {}".format(dir), file=sys.stderr)
|
||||
files = sorted(f for f in dir.iterdir() if f.suffix == ".md")
|
||||
for f in files:
|
||||
process_file(f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
|
||||
|
||||
out_all = (
|
||||
"// file generated by src/provider/update.py\n\n"
|
||||
"use crate::provider::Protocol::*;\n"
|
||||
"use crate::provider::Socket::*;\n"
|
||||
"use crate::provider::UsernamePattern::*;\n"
|
||||
"use crate::provider::{\n"
|
||||
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
|
||||
"};\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"use std::sync::LazyLock;\n\n"
|
||||
)
|
||||
|
||||
process_dir(Path(sys.argv[1]))
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
|
||||
out_all += out_domains
|
||||
out_all += "];\n\n"
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> = LazyLock::new(|| HashMap::from([\n"
|
||||
out_all += out_ids
|
||||
out_all += "]));\n\n"
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
now = datetime.datetime.utcnow()
|
||||
else:
|
||||
now = datetime.datetime.fromisoformat(sys.argv[2])
|
||||
out_all += (
|
||||
"pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> = "
|
||||
"LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
+ str(now.year)
|
||||
+ ", "
|
||||
+ str(now.month)
|
||||
+ ", "
|
||||
+ str(now.day)
|
||||
+ ").unwrap());\n"
|
||||
)
|
||||
|
||||
print(out_all)
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates provider database.
|
||||
# Returns 1 if the database is changed, 0 otherwise.
|
||||
set -euo pipefail
|
||||
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
|
||||
cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
|
||||
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
|
||||
rm -fr "$TMP"
|
||||
|
||||
cd "$CORE_ROOT"
|
||||
test -z "$(git status --porcelain src/provider/data.rs)"
|
||||
@@ -196,7 +196,7 @@ If the messenger finds a valid but not existent group-id,
|
||||
the messenger MAY create a new group.
|
||||
If no group-id is found,
|
||||
the message MAY be assigned
|
||||
to a single chat with the email-address given in `From`.
|
||||
to a normal single-user chat with the email-address given in `From`.
|
||||
|
||||
|
||||
## Add and remove members
|
||||
|
||||
+17
-24
@@ -452,10 +452,6 @@ impl Accounts {
|
||||
)
|
||||
.await
|
||||
{
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchTimedOut,
|
||||
});
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Warning("Background fetch timed out.".to_string()),
|
||||
@@ -465,16 +461,14 @@ impl Accounts {
|
||||
account_id = 0,
|
||||
"Background fetch timed out."
|
||||
);
|
||||
} else {
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
}
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
(*interrupt_sender.lock()) = None;
|
||||
}
|
||||
|
||||
// TODO adapt all the documentation
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// Ongoing background fetch can also be cancelled manually
|
||||
@@ -488,7 +482,10 @@ impl Accounts {
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
pub fn background_fetch(&self, timeout: std::time::Duration) {
|
||||
pub fn background_fetch(
|
||||
&self,
|
||||
timeout: std::time::Duration,
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
let (sender, receiver) = async_channel::bounded(1);
|
||||
@@ -503,17 +500,13 @@ impl Accounts {
|
||||
Some(receiver)
|
||||
}
|
||||
};
|
||||
let background_fetch_interrupt_sender = self.background_fetch_interrupt_sender.clone();
|
||||
tokio::task::spawn(async move {
|
||||
Self::background_fetch_with_timeout(
|
||||
accounts,
|
||||
events,
|
||||
timeout,
|
||||
background_fetch_interrupt_sender,
|
||||
receiver,
|
||||
)
|
||||
.await
|
||||
});
|
||||
Self::background_fetch_with_timeout(
|
||||
accounts,
|
||||
events,
|
||||
timeout,
|
||||
self.background_fetch_interrupt_sender.clone(),
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
/// Interrupts ongoing background_fetch() call,
|
||||
@@ -541,8 +534,8 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Sets notification token for Apple Push Notification service.
|
||||
pub fn set_push_device_token(&self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token);
|
||||
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use rand::seq::IndexedRandom;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
||||
|
||||
/// The target number of transports.
|
||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||
/// How often we want to try adding new relays.
|
||||
const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
||||
/// How long we ignore a relay candidate after failing to connect to it:
|
||||
const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||
|
||||
pub(crate) fn maybe_add_additional_relays(
|
||||
context: Context,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
// We need to Box::pin the future because it wouldn't compile otherwise
|
||||
// because Rust async doesn't support recursion:
|
||||
// `maybe_add_additional_relays_inner()` calls `restart_io_if_running()`,
|
||||
// which (via several other functions) calls `imap_loop()`,
|
||||
// which (via several other functions) calls `maybe_add_additional_relays()`
|
||||
Box::pin(async move {
|
||||
let skip_network = false;
|
||||
let relay_added = maybe_add_additional_relays_inner(&context, skip_network)
|
||||
.await
|
||||
.log_err(&context)
|
||||
.unwrap_or(false);
|
||||
|
||||
if relay_added {
|
||||
info!(context, "Restarting IO after relay addition");
|
||||
context.restart_io_if_running().await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool) -> Result<bool> {
|
||||
let now = time();
|
||||
|
||||
let Ok(_lock) = context.background_task_mutex.try_lock() else {
|
||||
// Housekeeping or automatic relay management is already running in another thread, do nothing.
|
||||
return Ok(false);
|
||||
};
|
||||
let last_timestamp = context
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
if last_timestamp > now {
|
||||
warn!(
|
||||
context,
|
||||
"Clock ran backwards, unclear if automatic relay management should run. Will run it anyways."
|
||||
);
|
||||
} else if last_timestamp > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) {
|
||||
return Ok(false);
|
||||
}
|
||||
if !context
|
||||
.get_config_bool(Config::AutomaticRelayManagement)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if context
|
||||
.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
// Set the config at the beginning to avoid endless loops.
|
||||
// Race conditions are not a concern because we locked the mutex.
|
||||
context
|
||||
.set_config_internal(Config::LastAutomaticRelayManagement, Some(&now.to_string()))
|
||||
.await?;
|
||||
|
||||
let mut relay_added = false;
|
||||
// Using `for` instead of `while` to prevent infinite loop
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::AutomaticRelayManagementFinished,
|
||||
config::from_bool(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
}
|
||||
|
||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
||||
// Hosts that are already used are excluded.
|
||||
let candidates = load_relay_candidates(context, now).await?;
|
||||
|
||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||
info!(
|
||||
context,
|
||||
"maybe_add_additional_relays: No suitable candidates"
|
||||
);
|
||||
return Ok(relay_added);
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Trying to automatically add relay {host} (there were {} candidates).",
|
||||
candidates.len(),
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
||||
(now, host),
|
||||
)
|
||||
.await?;
|
||||
let param = login_param_from_host(host);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to automatically add a relay {host}: {e:#}."
|
||||
);
|
||||
} else {
|
||||
info!(context, "Successfully automatically added relay {host}.");
|
||||
relay_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relay_added)
|
||||
}
|
||||
|
||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||
let candidates: Vec<String> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
// This also selects candidates which have last_tried in the future,
|
||||
// essentially treating them as never tried,
|
||||
// so if some timestamp far in the future is accidentally stored,
|
||||
// we are not stuck never trying the candidate.
|
||||
// After trying the candidate, last_tried will be corrected to the current time.
|
||||
"SELECT host FROM relay_candidates WHERE (last_tried<? OR last_tried>?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM transports
|
||||
WHERE substr(addr, instr(addr, '@') + 1) = host
|
||||
)",
|
||||
(cutoff_timestamp, now),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + host;
|
||||
let addr = addr_normalize(&addr);
|
||||
// 22 * log2(26 * 2 + 10) = 130 bits of entropy
|
||||
let password = Alphanumeric.sample_string(rng, 22);
|
||||
|
||||
EnteredLoginParam {
|
||||
addr,
|
||||
imap: EnteredImapLoginParam {
|
||||
password,
|
||||
..Default::default()
|
||||
},
|
||||
smtp: Default::default(),
|
||||
certificate_checks: EnteredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod automatic_relay_management_tests;
|
||||
@@ -1,286 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
|
||||
// This host should be returned by load_relay_candidates():
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("never_tried.example", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host was recently tried and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("recent.example", now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host is already in use (alice@example.org) and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("example.org", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let candidates = load_relay_candidates(t, now).await?;
|
||||
|
||||
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut candidates = load_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"a.example".to_string(),
|
||||
"b.example".to_string(),
|
||||
"c.example".to_string()
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_automatic_relay_management_does_nothing(t: &TestContext) {
|
||||
let transports_before = t.count_transports().await.unwrap();
|
||||
let config_before = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let skip_network = false; // No need to skip network, nothing is supposed to happen
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
let transports_after = t.count_transports().await.unwrap();
|
||||
|
||||
assert_eq!(config_after, config_before);
|
||||
assert_eq!(transports_before, transports_after);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_mutex_held() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
|
||||
// Hold the housekeeping mutex ourselves, simulating another task
|
||||
// already running housekeeping or relay management.
|
||||
let _lock = t.background_task_mutex.lock().await;
|
||||
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_debounce() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let some_seconds_ago = time() - 10;
|
||||
|
||||
// Pretend automatic relay management just ran.
|
||||
t.set_config_internal(
|
||||
Config::LastAutomaticRelayManagement,
|
||||
Some(&some_seconds_ago.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_disabled() {
|
||||
// By default, automatic relay management is disabled:
|
||||
let t = &TestContext::new_alice().await;
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
}
|
||||
|
||||
/// Runs maybe_add_additional_relays_inner(), then deletes one of the transports.
|
||||
/// Even after AUTOMATIC_ADDITION_DEBOUNCE_SECONDS,
|
||||
/// running automatic transport management again should not add back a transport.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_does_nothing_after_finishing_once() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
t.delete_transport(&transports.last().unwrap().param.addr)
|
||||
.await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(
|
||||
AUTOMATIC_ADDITION_DEBOUNCE_SECONDS as u64 + 1,
|
||||
));
|
||||
|
||||
let transports_count = t.count_transports().await?;
|
||||
assert_eq!(transports_count, NUM_TRANSPORTS_TARGET - 1);
|
||||
|
||||
assert!(
|
||||
t.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
);
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("relay.example", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, NUM_TRANSPORTS_TARGET);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for i in 1..10 {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(format!("{i}.invalid.example"), 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
// Don't skip network, since we want the relay addition to fail
|
||||
let skip_network = false;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
// The config is still updated:
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before);
|
||||
|
||||
// Some of the candidates should have an updated last_tried:
|
||||
assert!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM relay_candidates WHERE last_tried>=?",
|
||||
(now,)
|
||||
)
|
||||
.await?
|
||||
);
|
||||
|
||||
// ...but not all, because there might be many relay candidates
|
||||
// and we don't want to try all of them in a single call:
|
||||
assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn enable_config(context: &Context) {
|
||||
context
|
||||
.set_config_bool(Config::AutomaticRelayManagement, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
+10
-34
@@ -1,6 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -12,7 +12,7 @@ use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||
use num_traits::{FromPrimitive, cast};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
@@ -382,9 +382,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit,
|
||||
// as set by `Config::MediaQuality`.
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let mut target_wh = if exceeds_wh {
|
||||
max_wh
|
||||
} else {
|
||||
max(img.width(), img.height())
|
||||
};
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -423,35 +428,6 @@ impl<'a> BlobObject<'a> {
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
let longest_side_len = max(img.width(), img.height());
|
||||
|
||||
// target_wh will be used as the target-resolution for resizing the image,
|
||||
// so that the longest sides of the image match the target-resolution.
|
||||
let mut target_wh = if !is_avatar {
|
||||
let area_sqrt = (f64::from(img.width()) * f64::from(img.height())).sqrt();
|
||||
// Limit resolution to the number of pixels that fit within max_wh * max_wh,
|
||||
// so that the image-quality does not depend on the aspect-ratio.
|
||||
let mut resolution_limit: u32 = cast(
|
||||
(f64::from(longest_side_len) * (f64::from(max_wh) / area_sqrt)).floor(),
|
||||
)
|
||||
.unwrap_or(max_wh);
|
||||
// Align at least one dimension of the resampled image to a multiple of 8 pixels,
|
||||
// to have fewer partially used JPEG-blocks (which represent 8x8 pixels each).
|
||||
if resolution_limit < longest_side_len && resolution_limit > 8 {
|
||||
while !resolution_limit.is_multiple_of(8) {
|
||||
resolution_limit -= 1
|
||||
}
|
||||
}
|
||||
resolution_limit
|
||||
} else {
|
||||
max_wh
|
||||
};
|
||||
|
||||
target_wh = min(target_wh, longest_side_len);
|
||||
|
||||
// For images in JPEG-format, 65535 pixels is the maximum resolution per dimension.
|
||||
target_wh = min(target_wh, 65535);
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
@@ -573,7 +549,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
|
||||
&& let Some(val) = orientation.value.get_uint(0)
|
||||
&& let Ok(val) = TryInto::<u8>::try_into(val)
|
||||
{
|
||||
return Orientation::from_exif(val).unwrap_or_else(|| {
|
||||
return Orientation::from_exif(val).unwrap_or({
|
||||
warn!(context, "Exif orientation value ignored: {val:?}.");
|
||||
Orientation::NoTransforms
|
||||
});
|
||||
|
||||
@@ -406,8 +406,8 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -497,8 +497,8 @@ async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -517,8 +517,8 @@ async fn test_recode_image_huge_jpg() {
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1704,
|
||||
compressed_height: 959,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
|
||||
+12
-18
@@ -201,7 +201,7 @@ impl Context {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Can only place calls in single chats"
|
||||
"Can only place calls in 1:1 chats"
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
@@ -617,7 +617,7 @@ pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
|
||||
/// ICE server for JSON serialization.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub(crate) struct IceServer {
|
||||
struct IceServer {
|
||||
/// STUN or TURN URLs.
|
||||
pub urls: Vec<String>,
|
||||
|
||||
@@ -655,7 +655,7 @@ pub(crate) async fn create_ice_servers_from_metadata(
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
@@ -676,7 +676,7 @@ pub(crate) enum UnresolvedIceServer {
|
||||
pub(crate) async fn resolve_ice_servers(
|
||||
context: &Context,
|
||||
unresolved_ice_servers: Vec<UnresolvedIceServer>,
|
||||
) -> Result<Vec<IceServer>> {
|
||||
) -> Result<String> {
|
||||
let mut result: Vec<IceServer> = Vec::new();
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
@@ -733,7 +733,8 @@ pub(crate) async fn resolve_ice_servers(
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
@@ -769,19 +770,12 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
/// because it itself cannot utilize DNS. See
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
let mut unresolved_ice_servers: Vec<UnresolvedIceServer> = context
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.flat_map(|metadata| metadata.ice_servers.clone())
|
||||
.collect();
|
||||
unresolved_ice_servers.sort();
|
||||
unresolved_ice_servers.dedup();
|
||||
|
||||
let ice_servers = resolve_ice_servers(context, unresolved_ice_servers).await?;
|
||||
let json = serde_json::to_string(&ice_servers)?;
|
||||
Ok(json)
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// "Who can call me" config options.
|
||||
|
||||
+222
-272
@@ -35,26 +35,23 @@ use crate::download::{
|
||||
use crate::ensure_and_debug_assert_eq;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::reaction::broadcast_reactions;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_secret, create_id,
|
||||
create_outgoing_rfc724_mid, get_abs_path, gm2local_offset, normalize_text, time,
|
||||
truncate_msg_text,
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
|
||||
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
@@ -100,7 +97,7 @@ pub(crate) enum CantSendReason {
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
|
||||
/// State for single chat with a key-contact that does not have a key.
|
||||
/// State for 1:1 chat with a key-contact that does not have a key.
|
||||
MissingKey,
|
||||
}
|
||||
|
||||
@@ -197,7 +194,7 @@ impl ChatId {
|
||||
Some(msg.chat_id)
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] for the single chat with `contact_id`
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
|
||||
/// if it exists and is not blocked.
|
||||
///
|
||||
/// If the chat does not exist or is blocked, `None` is returned.
|
||||
@@ -217,7 +214,7 @@ impl ChatId {
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] for the single chat with `contact_id`.
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`.
|
||||
///
|
||||
/// If the chat does not yet exist an unblocked chat ([`Blocked::Not`]) is created.
|
||||
///
|
||||
@@ -230,9 +227,9 @@ impl ChatId {
|
||||
.map(|chat| chat.id)
|
||||
}
|
||||
|
||||
/// Returns the unblocked single chat with `contact_id`.
|
||||
/// Returns the unblocked 1:1 chat with `contact_id`.
|
||||
///
|
||||
/// This should be used when **a user action** creates a single chat, it ensures the chat
|
||||
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
|
||||
/// exists, is unblocked and scales the [`Contact`]'s origin.
|
||||
pub async fn create_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
|
||||
@@ -295,7 +292,7 @@ impl ChatId {
|
||||
timestamp: i64,
|
||||
) -> Result<Self> {
|
||||
let grpname = sanitize_single_line(grpname);
|
||||
let timestamp = cmp::min(timestamp, time());
|
||||
let timestamp = cmp::min(timestamp, smeared_time(context));
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, name_normalized, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, 0, ?)",
|
||||
@@ -377,7 +374,7 @@ impl ChatId {
|
||||
if contact_id != ContactId::SELF {
|
||||
info!(
|
||||
context,
|
||||
"Blocking the contact {contact_id} to block a single chat."
|
||||
"Blocking the contact {contact_id} to block 1:1 chat."
|
||||
);
|
||||
contact::set_blocked(context, Nosync, contact_id, true).await?;
|
||||
}
|
||||
@@ -396,7 +393,7 @@ impl ChatId {
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a single chat this currently triggers `Contact::block()` on other devices.
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
chat.sync(context, SyncAction::Block)
|
||||
.await
|
||||
.log_err(context)
|
||||
@@ -420,7 +417,7 @@ impl ChatId {
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
// TODO: For a single chat this currently triggers `Contact::unblock()` on other devices.
|
||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
|
||||
// with `block()` which also blocks the contact.
|
||||
chat.sync(context, SyncAction::Unblock)
|
||||
@@ -780,14 +777,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
}
|
||||
|
||||
/// Set provided message as draft message for specified chat.
|
||||
///
|
||||
/// If there is an existing draft message,
|
||||
/// this function tries to update it instead of creating a new one,
|
||||
/// thus preserving the ID and possible WebXDC status updates
|
||||
/// associated with the draft message.
|
||||
///
|
||||
/// Returns `false` if the existing draft is already at the state
|
||||
/// that the caller tried to set it to, so it was unchanged.
|
||||
/// Returns true if the draft was added or updated in place.
|
||||
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
|
||||
match msg.viewtype {
|
||||
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
|
||||
@@ -1164,7 +1154,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
MessageState::InSeen as u32,
|
||||
state_out_min as u32,
|
||||
// Do not reply to not fully downloaded messages. Such a message could be a group chat
|
||||
// message that we assigned to a single chat.
|
||||
// message that we assigned to 1:1 chat.
|
||||
DownloadState::Done as u32,
|
||||
// Do not reference info messages, they are not actually sent out
|
||||
// and have Message-IDs unknown to other chat members.
|
||||
@@ -1266,7 +1256,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
message_timestamp: i64,
|
||||
always_sort_to_bottom: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, time());
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
|
||||
|
||||
let last_msg_time: Option<i64> = if always_sort_to_bottom {
|
||||
// get newest message for this chat
|
||||
@@ -1357,7 +1347,7 @@ pub struct Chat {
|
||||
/// Database ID.
|
||||
pub id: ChatId,
|
||||
|
||||
/// Chat type, e.g. a single chat, group chat, mailing list.
|
||||
/// Chat type, e.g. 1:1 chat, group chat, mailing list.
|
||||
pub typ: Chattype,
|
||||
|
||||
/// Chat name.
|
||||
@@ -1366,7 +1356,7 @@ pub struct Chat {
|
||||
/// Whether the chat is archived or pinned.
|
||||
pub visibility: ChatVisibility,
|
||||
|
||||
/// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for single chats and
|
||||
/// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
|
||||
/// ad-hoc groups.
|
||||
pub grpid: String,
|
||||
|
||||
@@ -1594,7 +1584,7 @@ impl Chat {
|
||||
Path::new(&get_unencrypted_icon(context).await?),
|
||||
)));
|
||||
} else if self.typ == Chattype::Single {
|
||||
// For single chats, we always use the same avatar as for the contact
|
||||
// For 1:1 chats, we always use the same avatar as for the contact
|
||||
// This is before the `self.is_encrypted()` check, because that function
|
||||
// has two database calls, i.e. it's slow
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
@@ -1613,7 +1603,7 @@ impl Chat {
|
||||
|
||||
/// Returns chat avatar color.
|
||||
///
|
||||
/// For single chats, the color is calculated from the contact's address
|
||||
/// For 1:1 chats, the color is calculated from the contact's address
|
||||
/// for address-contacts and from the OpenPGP key fingerprint for key-contacts.
|
||||
/// For group chats the color is calculated from the grpid, if present, or the chat name.
|
||||
pub async fn get_color(&self, context: &Context) -> Result<u32> {
|
||||
@@ -1804,7 +1794,8 @@ impl Chat {
|
||||
}
|
||||
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
msg.param.set_optional(Param::Bot, is_bot.then_some("1"));
|
||||
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.
|
||||
@@ -1902,7 +1893,7 @@ impl Chat {
|
||||
};
|
||||
let ephemeral_timestamp = match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.get().into()),
|
||||
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
|
||||
};
|
||||
|
||||
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
|
||||
@@ -2348,7 +2339,7 @@ pub(crate) struct ChatIdBlocked {
|
||||
}
|
||||
|
||||
impl ChatIdBlocked {
|
||||
/// Searches the database for the single chat with this contact.
|
||||
/// Searches the database for the 1:1 chat with this contact.
|
||||
///
|
||||
/// If no chat is found `None` is returned.
|
||||
pub async fn lookup_by_contact(
|
||||
@@ -2381,7 +2372,7 @@ impl ChatIdBlocked {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the chat for the single chat with this contact.
|
||||
/// Returns the chat for the 1:1 chat with this contact.
|
||||
///
|
||||
/// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
|
||||
/// state.
|
||||
@@ -2414,7 +2405,7 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let now = time();
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
.sql
|
||||
@@ -2429,7 +2420,7 @@ impl ChatIdBlocked {
|
||||
normalize_text(&chat_name),
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
now,
|
||||
smeared_time,
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
@@ -2455,7 +2446,7 @@ impl ChatIdBlocked {
|
||||
&& !chat.param.exists(Param::Devicetalk)
|
||||
&& !chat.param.exists(Param::Selftalk)
|
||||
{
|
||||
chat_id.add_e2ee_notice(context, now).await?;
|
||||
chat_id.add_e2ee_notice(context, smeared_time).await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -2468,121 +2459,118 @@ impl ChatIdBlocked {
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::Call {
|
||||
// the caller should check if the message text is empty
|
||||
return Ok(());
|
||||
}
|
||||
if !msg.viewtype.has_file() {
|
||||
} else if msg.viewtype.has_file() {
|
||||
let viewtype_orig = msg.viewtype;
|
||||
let mut blob = msg
|
||||
.param
|
||||
.get_file_blob(context)?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let mut maybe_image = false;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
// Typical conversions:
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
|
||||
if better_type == Viewtype::Image {
|
||||
maybe_image = true;
|
||||
} else if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
|
||||
let new_name = blob
|
||||
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
|
||||
.await?;
|
||||
msg.param.set(Param::Filename, new_name);
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
}
|
||||
|
||||
if !msg.param.exists(Param::MimeType)
|
||||
&& let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg)
|
||||
{
|
||||
// If we unexpectedly didn't recognize the file as image, don't send it as such,
|
||||
// either the format is unsupported or the image is corrupted.
|
||||
let mime = match viewtype != Viewtype::Image
|
||||
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
|
||||
{
|
||||
true => mime,
|
||||
false => "application/octet-stream",
|
||||
};
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
}
|
||||
|
||||
msg.try_calc_and_set_dimensions(context).await?;
|
||||
|
||||
let filename = msg.get_filename().context("msg has no file")?;
|
||||
let suffix = Path::new(&filename)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("dat");
|
||||
// Get file name to use for sending. For privacy purposes, we do not transfer the original
|
||||
// filenames e.g. for images; these names are normally not needed and contain timestamps,
|
||||
// running numbers, etc.
|
||||
let filename: String = match viewtype_orig {
|
||||
Viewtype::Voice => format!(
|
||||
"voice-messsage_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"image_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
),
|
||||
),
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
),
|
||||
_ => filename,
|
||||
};
|
||||
msg.param.set(Param::Filename, filename);
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Attaching \"{}\" for message type #{}.",
|
||||
blob.to_abs_path().display(),
|
||||
msg.viewtype
|
||||
);
|
||||
} else {
|
||||
bail!("Cannot send messages of type #{}.", msg.viewtype);
|
||||
}
|
||||
|
||||
let viewtype_orig = msg.viewtype;
|
||||
let mut blob = msg
|
||||
.param
|
||||
.get_file_blob(context)?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let mut maybe_image = false;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
// Typical conversions:
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
|
||||
if better_type == Viewtype::Image {
|
||||
maybe_image = true;
|
||||
} else if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
|
||||
let new_name = blob
|
||||
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
|
||||
.await?;
|
||||
msg.param.set(Param::Filename, new_name);
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
}
|
||||
|
||||
if !msg.param.exists(Param::MimeType)
|
||||
&& let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg)
|
||||
{
|
||||
// If we unexpectedly didn't recognize the file as image, don't send it as such,
|
||||
// either the format is unsupported or the image is corrupted.
|
||||
let mime = match viewtype != Viewtype::Image
|
||||
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
|
||||
{
|
||||
true => mime,
|
||||
false => "application/octet-stream",
|
||||
};
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
}
|
||||
|
||||
msg.try_calc_and_set_dimensions(context).await?;
|
||||
|
||||
let filename = msg.get_filename().context("msg has no file")?;
|
||||
let suffix = Path::new(&filename)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("dat");
|
||||
// Get file name to use for sending. For privacy purposes, we do not transfer the original
|
||||
// filenames e.g. for images; these names are normally not needed and contain timestamps,
|
||||
// running numbers, etc.
|
||||
let filename: String = match viewtype_orig {
|
||||
Viewtype::Voice => format!(
|
||||
"voice-messsage_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"image_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
),
|
||||
),
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{suffix}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
),
|
||||
_ => filename,
|
||||
};
|
||||
msg.param.set(Param::Filename, filename);
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Attaching \"{}\" for message type #{}.",
|
||||
blob.to_abs_path().display(),
|
||||
msg.viewtype
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2592,11 +2580,11 @@ pub async fn is_contact_in_chat(
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
// this function works for group and for single chats, however, it is more useful
|
||||
// this function works for group and for normal chats, however, it is more useful
|
||||
// for group chats.
|
||||
// ContactId::SELF may be used to check whether oneself
|
||||
// is in a group or incoming broadcast chat
|
||||
// (ContactId::SELF is not added to single chats or outgoing broadcast channels)
|
||||
// (ContactId::SELF is not added to 1:1 chats or outgoing broadcast channels)
|
||||
|
||||
let exists = context
|
||||
.sql
|
||||
@@ -2688,11 +2676,10 @@ async fn prepare_send_msg(
|
||||
// from the chat.
|
||||
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||
CantSendReason::InBroadcast => {
|
||||
msg.param.get_int(Param::Reaction).unwrap_or_default() != 0
|
||||
|| matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||
)
|
||||
matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||
)
|
||||
}
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
@@ -2743,7 +2730,7 @@ async fn prepare_send_msg(
|
||||
}
|
||||
msg.state = MessageState::OutPending;
|
||||
|
||||
msg.timestamp_sort = time();
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
@@ -2770,10 +2757,6 @@ async fn render_mime_message_and_pre_message(
|
||||
msg: &mut Message,
|
||||
mimefactory: MimeFactory,
|
||||
) -> Result<(Option<RenderedEmail>, RenderedEmail)> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
|
||||
let needs_pre_message = msg.viewtype.has_file()
|
||||
&& mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages
|
||||
&& msg
|
||||
@@ -2790,31 +2773,15 @@ async fn render_mime_message_and_pre_message(
|
||||
|
||||
let mut mimefactory_post_msg = mimefactory.clone();
|
||||
mimefactory_post_msg.set_as_post_message();
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory_post_msg.into_queued_mail(context))
|
||||
let rendered_msg = Box::pin(mimefactory_post_msg.render(context))
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr.clone(),
|
||||
side_effects,
|
||||
)?;
|
||||
|
||||
let mut mimefactory_pre_msg = mimefactory;
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
|
||||
let (queued_pre_msg, pre_side_effects) =
|
||||
Box::pin(mimefactory_pre_msg.into_queued_mail(context))
|
||||
.await
|
||||
.context("pre-message failed to render")?;
|
||||
let rendered_pre_msg = mimefactory::render_queued_mail(
|
||||
queued_pre_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
pre_side_effects,
|
||||
)?;
|
||||
let rendered_pre_msg = Box::pin(mimefactory_pre_msg.render(context))
|
||||
.await
|
||||
.context("pre-message failed to render")?;
|
||||
|
||||
if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD {
|
||||
warn!(
|
||||
@@ -2827,16 +2794,7 @@ async fn render_mime_message_and_pre_message(
|
||||
|
||||
Ok((Some(rendered_pre_msg), rendered_msg))
|
||||
} else {
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory.into_queued_mail(context)).await?;
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
side_effects,
|
||||
)?;
|
||||
|
||||
Ok((None, rendered_msg))
|
||||
Ok((None, Box::pin(mimefactory.render(context)).await?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2881,8 +2839,14 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out:
|
||||
if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden)
|
||||
// This may happen eg. for groups with only SELF and bcc_self disabled:
|
||||
@@ -2960,24 +2924,17 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
let now = smeared_time(context);
|
||||
|
||||
if let Some(last_added_location_timestamp) =
|
||||
rendered_msg.side_effects.last_added_location_timestamp
|
||||
if rendered_msg.last_added_location_id.is_some()
|
||||
&& let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await
|
||||
{
|
||||
location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp)
|
||||
.await?;
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
}
|
||||
|
||||
if rendered_msg.side_effects.avatar_is_attached
|
||||
|| rendered_pre_msg
|
||||
.as_ref()
|
||||
.is_some_and(|msg| msg.side_effects.avatar_is_attached)
|
||||
if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await
|
||||
{
|
||||
msg.chat_id
|
||||
.set_selfavatar_timestamp(context, now)
|
||||
.await
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted {
|
||||
@@ -2985,7 +2942,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
} else {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
msg.subject.clone_from(&rendered_msg.side_effects.subject);
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
context
|
||||
.sql
|
||||
@@ -3014,25 +2971,26 @@ WHERE id=?
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.side_effects.sync_ids_to_delete {
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
t.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
if !recipients.is_empty() {
|
||||
let mut stmt = t.prepare(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
)?;
|
||||
let all_recipients = recipients.join(" ");
|
||||
let mut stmt = t.prepare(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
)?;
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
if let Some(pre_msg) = &rendered_pre_msg {
|
||||
let row_id = stmt.execute((
|
||||
&pre_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&recipients_chunk,
|
||||
&pre_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
@@ -3040,7 +2998,7 @@ WHERE id=?
|
||||
}
|
||||
let row_id = stmt.execute((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
@@ -3088,7 +3046,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
save_text_edit_to_db(context, &mut original_msg, &new_text, &[]).await?;
|
||||
save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
|
||||
|
||||
let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
|
||||
edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
|
||||
@@ -3107,20 +3065,16 @@ pub(crate) async fn save_text_edit_to_db(
|
||||
context: &Context,
|
||||
original_msg: &mut Message,
|
||||
new_text: &str,
|
||||
mime_headers: &[u8],
|
||||
) -> Result<()> {
|
||||
original_msg.param.set_int(Param::IsEdited, 1);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET txt=?, txt_normalized=?, param=?, mime_headers=?, mime_modified=? WHERE id=?",
|
||||
"UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
|
||||
(
|
||||
new_text,
|
||||
normalize_text(new_text),
|
||||
original_msg.param.to_string(),
|
||||
mime_headers,
|
||||
!mime_headers.is_empty(),
|
||||
original_msg.id,
|
||||
),
|
||||
)
|
||||
@@ -3254,8 +3208,8 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> {
|
||||
AND c.blocked=0;",
|
||||
(MessageState::InFresh,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
let msg_id: ChatId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -3537,7 +3491,7 @@ pub async fn get_chat_media(
|
||||
|
||||
/// Returns a vector of contact IDs for given chat ID.
|
||||
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
|
||||
// Single chats do not include SELF. Group chats do (as it may happen that one is deleted from a
|
||||
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
|
||||
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
|
||||
context
|
||||
.sql
|
||||
@@ -3598,10 +3552,6 @@ pub async fn create_group_unencrypted(context: &Context, name: &str) -> Result<C
|
||||
/// unencrypted chats currently.
|
||||
/// * `grpid` - Group ID. Iff nonempty, the chat is encrypted (with key-contacts).
|
||||
/// * `name` - Chat name.
|
||||
///
|
||||
/// NB: Unencrypted chats with similar names and the same members are merged on other devices, but
|
||||
/// usually users don't create such chats and look up the existing one instead, so chat split on the
|
||||
/// first device is acceptable.
|
||||
pub(crate) async fn create_group_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
@@ -3616,7 +3566,7 @@ pub(crate) async fn create_group_ex(
|
||||
chat_name = "…".to_string();
|
||||
}
|
||||
|
||||
let timestamp = time();
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -3699,7 +3649,7 @@ pub(crate) async fn create_out_broadcast_ex(
|
||||
bail!("Invalid broadcast channel name: {chat_name}.");
|
||||
}
|
||||
|
||||
let timestamp = time();
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| -> Result<ChatId> {
|
||||
let cnt: u32 = t.query_row(
|
||||
"SELECT COUNT(*) FROM chats WHERE grpid=?",
|
||||
@@ -3712,15 +3662,14 @@ pub(crate) async fn create_out_broadcast_ex(
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
(type, name, name_normalized, grpid, created_timestamp, muted_until, param)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?)",
|
||||
(type, name, name_normalized, grpid, created_timestamp, param)
|
||||
VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
Chattype::OutBroadcast,
|
||||
&chat_name,
|
||||
normalize_text(&chat_name),
|
||||
&grpid,
|
||||
timestamp,
|
||||
MuteDuration::Forever,
|
||||
params.to_string(),
|
||||
),
|
||||
)?;
|
||||
@@ -3789,17 +3738,19 @@ pub(crate) async fn update_chat_contacts_table(
|
||||
id: ChatId,
|
||||
contacts: &BTreeSet<ContactId>,
|
||||
) -> Result<()> {
|
||||
// See add_to_chat_contacts_table() for reasoning.
|
||||
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
// Bump `remove_timestamp` to at least `now`
|
||||
// even for members from `contacts`.
|
||||
// Bump `remove_timestamp` even for members from `contacts`.
|
||||
// We add members from `contacts` back below.
|
||||
transaction.execute(
|
||||
"UPDATE chats_contacts
|
||||
SET remove_timestamp=MAX(add_timestamp+1, ?)
|
||||
"UPDATE chats_contacts SET
|
||||
add_timestamp=MIN(add_timestamp, ?1),
|
||||
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
|
||||
WHERE chat_id=?",
|
||||
(timestamp, id),
|
||||
(limit, timestamp, id),
|
||||
)?;
|
||||
|
||||
if !contacts.is_empty() {
|
||||
@@ -3811,9 +3762,8 @@ pub(crate) async fn update_chat_contacts_table(
|
||||
)?;
|
||||
|
||||
for contact_id in contacts {
|
||||
// We bumped `add_timestamp` for existing rows above,
|
||||
// so on conflict it is enough to set `add_timestamp = remove_timestamp`
|
||||
// and this guarantees that `add_timestamp` is no less than `timestamp`.
|
||||
// We bumped `remove_timestamp` for existing rows above,
|
||||
// so on conflict it is enough to set `add_timestamp = remove_timestamp`.
|
||||
statement.execute((id, contact_id, timestamp))?;
|
||||
}
|
||||
}
|
||||
@@ -3830,17 +3780,24 @@ pub(crate) async fn add_to_chat_contacts_table(
|
||||
chat_id: ChatId,
|
||||
contact_ids: &[ContactId],
|
||||
) -> Result<()> {
|
||||
// Our clock may be slow, so limit stored timestamps with `timestamp` if it's bigger. This way
|
||||
// we only cap remote timestamps if, in addition, remote changes arrive reordered or we do local
|
||||
// changes. Also allow some tolerance, moreover, previous removals might lend time from the
|
||||
// future.
|
||||
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let mut add_statement = transaction.prepare(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3)
|
||||
ON CONFLICT (chat_id, contact_id)
|
||||
DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
|
||||
DO UPDATE SET
|
||||
remove_timestamp=MIN(remove_timestamp, ?4),
|
||||
add_timestamp=MIN(MAX(add_timestamp,remove_timestamp,?3), ?4)",
|
||||
)?;
|
||||
|
||||
for contact_id in contact_ids {
|
||||
add_statement.execute((chat_id, contact_id, timestamp))?;
|
||||
add_statement.execute((chat_id, contact_id, timestamp, limit))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -3859,13 +3816,16 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
let now = time();
|
||||
// See add_to_chat_contacts_table() for reasoning.
|
||||
let limit = now.saturating_add(TIMESTAMP_SENT_TOLERANCE);
|
||||
let is_past_member = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats_contacts
|
||||
SET remove_timestamp=MAX(add_timestamp+1, ?)
|
||||
"UPDATE chats_contacts SET
|
||||
add_timestamp=MIN(add_timestamp, ?1),
|
||||
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
|
||||
WHERE chat_id=? AND contact_id=?",
|
||||
(now, chat_id, contact_id),
|
||||
(limit, now, chat_id, contact_id),
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
@@ -3922,7 +3882,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
|
||||
chat_id.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
// this also makes sure, no contacts are added to special or single chats
|
||||
// this also makes sure, no contacts are added to special or normal chats
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || (from_handshake && chat.typ == Chattype::OutBroadcast),
|
||||
@@ -3958,11 +3918,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
return Ok(false);
|
||||
}
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
let now = time();
|
||||
let smeared_time = smeared_time(context);
|
||||
chat.param
|
||||
.remove(Param::Unpromoted)
|
||||
.set_i64(Param::GroupNameTimestamp, now)
|
||||
.set_i64(Param::GroupDescriptionTimestamp, now);
|
||||
.set_i64(Param::GroupNameTimestamp, smeared_time)
|
||||
.set_i64(Param::GroupDescriptionTimestamp, smeared_time);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -4395,7 +4355,7 @@ async fn rename_ex(
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let new_name = sanitize_single_line(new_name);
|
||||
/* the function only sets the names of group chats; single chats get their names from the contacts */
|
||||
/* the function only sets the names of group chats; normal chats get their names from the contacts */
|
||||
let mut success = false;
|
||||
|
||||
ensure!(!new_name.is_empty(), "Invalid name");
|
||||
@@ -4528,6 +4488,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
}
|
||||
|
||||
/// Forwards multiple messages to a chat in another context.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn forward_msgs_2ctx(
|
||||
ctx_src: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
@@ -4538,6 +4499,7 @@ pub async fn forward_msgs_2ctx(
|
||||
ensure!(!chat_id.is_special(), "can not forward to special chat");
|
||||
|
||||
let mut created_msgs: Vec<MsgId> = Vec::new();
|
||||
let mut curr_timestamp: i64;
|
||||
|
||||
chat_id
|
||||
.unarchive_if_not_muted(ctx_dst, MessageState::Undefined)
|
||||
@@ -4546,7 +4508,7 @@ pub async fn forward_msgs_2ctx(
|
||||
if let Some(reason) = chat.why_cant_send(ctx_dst).await? {
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
let now = time();
|
||||
curr_timestamp = create_smeared_timestamps(ctx_dst, msg_ids.len());
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids {
|
||||
let ts: i64 = ctx_src
|
||||
@@ -4611,9 +4573,10 @@ pub async fn forward_msgs_2ctx(
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.pre_rfc724_mid.clear();
|
||||
msg.timestamp_sort = now;
|
||||
msg.timestamp_sort = curr_timestamp;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
||||
ctx_dst.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
@@ -4706,7 +4669,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
} else {
|
||||
MessageState::InSeen
|
||||
},
|
||||
time(),
|
||||
create_smeared_timestamp(context),
|
||||
msg.param.to_string(),
|
||||
src_msg_id,
|
||||
src_msg_id,
|
||||
@@ -4771,11 +4734,6 @@ pub(crate) async fn resend_msgs_ex(
|
||||
}
|
||||
if let Some(to_fingerprint) = &to_fingerprint {
|
||||
msg.param.set(Param::Arg4, to_fingerprint.clone());
|
||||
if let Some(json) = broadcast_reactions::render_json(context, &[msg.id]).await? {
|
||||
// The returned reaction array for the message may be empty,
|
||||
// so rejoining members get reactions cleared as neccessary.
|
||||
msg.param.set(Param::BroadcastReactions, json);
|
||||
}
|
||||
}
|
||||
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
continue;
|
||||
@@ -4888,7 +4846,7 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
@@ -5035,7 +4993,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
} else {
|
||||
let sort_to_bottom = true;
|
||||
chat_id
|
||||
.calc_sort_timestamp(context, time(), sort_to_bottom)
|
||||
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -5198,7 +5156,7 @@ async fn set_contacts_by_fingerprints(
|
||||
Ok(broadcast_contacts_added)
|
||||
})
|
||||
.await?;
|
||||
let timestamp = time();
|
||||
let timestamp = smeared_time(context);
|
||||
for added_id in broadcast_contacts_added {
|
||||
let msg = stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED).await;
|
||||
add_info_msg_with_cmd(
|
||||
@@ -5282,11 +5240,9 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
// Newly created chat will be soon unblocked, `Blocked::Yes` here is just
|
||||
// to hide it from chatlist, as at this point it is completely blank (no name etc.).
|
||||
// Even if app crashes at this point, the chat will re-appear on e.g. any new
|
||||
// message sent from other device.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Yes)
|
||||
// Use `Request` so that even if the program crashes, the user doesn't have to look
|
||||
// into the blocked contacts.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
@@ -5310,13 +5266,7 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
// Don't show a chat on other devices until securejoin completes.
|
||||
// E.g. pinning a not-synced chat on device A shouldn't display it on device B yet.
|
||||
//
|
||||
// A pinned chat will appear on other devices only when the first
|
||||
// message is sent from the device that read the invitation - which is the same
|
||||
// behavior as with un-pinned chats.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Yes)
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
|
||||
+57
-271
@@ -1,13 +1,9 @@
|
||||
use std::num::NonZero;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::Event;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{
|
||||
self, DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER,
|
||||
};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
@@ -23,6 +19,7 @@ use crate::test_utils::{
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -260,9 +257,9 @@ async fn test_quote_replies() -> Result<()> {
|
||||
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 single_chat_id = alice.create_chat(&bob).await.id;
|
||||
let single_chat_msg_id = send_text_msg(&alice, single_chat_id, "foo".to_string()).await?;
|
||||
let single_chat_msg = Message::load_from_db(&alice, single_chat_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_text("baz".to_string());
|
||||
@@ -271,25 +268,25 @@ async fn test_quote_replies() -> Result<()> {
|
||||
assert!(result.is_ok());
|
||||
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&single_chat_msg)).await?;
|
||||
let result = send_msg(&alice, single_chat_id, &mut msg).await;
|
||||
msg.set_quote(&alice, Some(&one2one_msg)).await?;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
let single_chat_quote_reply_msg_id = result.unwrap();
|
||||
let one2one_quote_reply_msg_id = result.unwrap();
|
||||
|
||||
// quoting messages from groups to single chats is okay ("reply privately")
|
||||
// quoting messages from groups to one-to-ones is okay ("reply privately")
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&grp_msg)).await?;
|
||||
let result = send_msg(&alice, single_chat_id, &mut msg).await;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// quoting messages from single chats in groups is an error; usually this is also not allowed by UI at all ...
|
||||
// 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_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&single_chat_msg)).await?;
|
||||
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, &[single_chat_quote_reply_msg_id], grp_chat_id).await;
|
||||
let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// ... and bots are not restricted
|
||||
@@ -751,7 +748,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InNoticed);
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
@@ -776,7 +773,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that adding or removing contacts in single chat is not allowed.
|
||||
/// Test that adding or removing contacts in 1:1 chat is not allowed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_remove_contact_for_single() {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
@@ -786,7 +783,7 @@ async fn test_add_remove_contact_for_single() {
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1);
|
||||
|
||||
// adding or removing contacts from single chats result in an error
|
||||
// adding or removing contacts from one-to-one-chats result in an error
|
||||
let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
|
||||
let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await;
|
||||
assert!(added.is_err());
|
||||
@@ -1284,7 +1281,7 @@ async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
|
||||
tcm.section("bob: receive messages, accept all chats and send a reply to each messsage");
|
||||
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt().await {
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await {
|
||||
let bob_message = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = bob_message.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
@@ -1292,7 +1289,7 @@ async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
}
|
||||
|
||||
tcm.section("alice: receive replies from bob");
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt().await {
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await {
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
}
|
||||
// ensure chats have unread messages
|
||||
@@ -1375,18 +1372,6 @@ async fn test_markfresh_chat() -> Result<()> {
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// Marking a message as seen results to sending an MDN to the contact and self.
|
||||
message::markseen_msgs(bob, vec![bob_msg2.id]).await?;
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
|
||||
(bob_msg2.from_id,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
|
||||
// bob marks the chat as fresh again, fresh count is 1 again
|
||||
markfresh_chat(bob, bob_chat_id).await?;
|
||||
let bob_msg1 = Message::load_from_db(bob, bob_msg1.id).await?;
|
||||
@@ -1648,7 +1633,6 @@ async fn test_set_chat_name() {
|
||||
"another name",
|
||||
"something different",
|
||||
] {
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, chat_id, new_name).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -2831,7 +2815,7 @@ async fn test_cant_remove_nonmember() -> Result<()> {
|
||||
|
||||
let alice_charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
remove_contact_from_chat(alice, alice_broadcast_id, alice_charlie_id).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
assert!(!remove_from_chat_contacts_table(alice, alice_broadcast_id, alice_charlie_id).await?);
|
||||
assert!(
|
||||
!remove_from_chat_contacts_table_without_trace(alice, alice_broadcast_id, alice_charlie_id)
|
||||
@@ -2986,7 +2970,6 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
|
||||
{
|
||||
tcm.section("Alice changes the chat name");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, broadcast_id, "My great broadcast").await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -3005,7 +2988,6 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
|
||||
{
|
||||
tcm.section("Alice changes the chat name again, but the system message is lost somehow");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, broadcast_id, "Broadcast channel").await?;
|
||||
|
||||
let chat = Chat::load_from_db(alice, broadcast_id).await?;
|
||||
@@ -3056,29 +3038,6 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_muted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Alice's new outgoing broadcast channel is muted after creation:
|
||||
// Channel owners can only get reaction notifications; they are usually not of much interest.
|
||||
let alice_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||
assert!(alice_chat.is_muted());
|
||||
|
||||
// Bob joins the channel, for him, it is not muted:
|
||||
// For channel subscribers, new messages to newly subscribed channels are often interesting.
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert!(!bob_chat.is_muted());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3105,11 +3064,13 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
}
|
||||
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
|
||||
let rev_order = false;
|
||||
let resent_msg = alice.pop_sent_msg_ex(rev_order).await.unwrap();
|
||||
let resent_msg = alice
|
||||
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||
.await
|
||||
.unwrap();
|
||||
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||
assert_eq!(fiona_msg.text, (i + 1).to_string());
|
||||
assert_eq!(fiona_msg.param.get_bool(Param::WantsMdn).unwrap(), true);
|
||||
assert!(resent_msg.recipients.contains("fiona@example.net"));
|
||||
assert!(!resent_msg.recipients.contains("bob@"));
|
||||
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
|
||||
@@ -3123,7 +3084,7 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
);
|
||||
bob.recv_msg_trash(&resent_msg).await;
|
||||
}
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3381,7 +3342,6 @@ async fn test_broadcasts_name_and_avatar() -> Result<()> {
|
||||
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||
|
||||
tcm.section("Change broadcast channel name, and check that receivers see it");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, alice_chat_id, "New Channel name").await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
@@ -3547,9 +3507,8 @@ async fn test_chat_description(
|
||||
"",
|
||||
"ä ẟ 😂",
|
||||
] {
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
tcm.section(&format!(
|
||||
"Alice sets the chat description to {description:?}"
|
||||
"Alice sets the chat description to '{description}'"
|
||||
));
|
||||
set_chat_description(alice, alice_chat_id, description).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -3588,7 +3547,12 @@ async fn test_chat_description(
|
||||
|
||||
tcm.section("Alice calls set_chat_description() without actually changing the description");
|
||||
set_chat_description(alice, alice_chat_id, "ä ẟ 😂").await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(
|
||||
alice
|
||||
.pop_sent_msg_opt(Duration::from_secs(0))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3613,7 +3577,12 @@ async fn test_setting_empty_chat_description() -> Result<()> {
|
||||
let _hi = alice.send_text(alice_chat_id, "hi").await;
|
||||
|
||||
set_chat_description(alice, alice_chat_id, "").await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(
|
||||
alice
|
||||
.pop_sent_msg_opt(Duration::from_secs(0))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3648,7 +3617,7 @@ async fn test_broadcast_joining_golden() -> Result<()> {
|
||||
.await;
|
||||
|
||||
let alice_bob_contact = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
// The single chat with Bob should not be visible to the user:
|
||||
// The 1:1 chat with Bob should not be visible to the user:
|
||||
assert!(
|
||||
ChatIdBlocked::lookup_by_contact(alice, alice_bob_contact.id)
|
||||
.await?
|
||||
@@ -3961,11 +3930,11 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
bob1.recv_msg_trash(&request_with_auth).await;
|
||||
bob1.recv_msg(&member_added).await;
|
||||
|
||||
// The single chat should not be visible to the user on any of the devices.
|
||||
// The 1:1 chat should not be visible to the user on any of the devices.
|
||||
// The contact should be marked as verified.
|
||||
check_single_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
|
||||
check_single_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
|
||||
check_single_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
|
||||
check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
@@ -3980,7 +3949,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
|
||||
let leave_msg = bob0.pop_sent_msg().await;
|
||||
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes()).await?;
|
||||
assert_eq!(parsed.parts[0].msg, "Member bob@example.net was removed.");
|
||||
assert_eq!(parsed.parts[0].msg, "bob@example.net left the group.");
|
||||
|
||||
let rcvd = bob1.recv_msg(&leave_msg).await;
|
||||
|
||||
@@ -3992,16 +3961,16 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_single_chat_is_hidden_and_contact_is_verified(
|
||||
async fn check_direct_chat_is_hidden_and_contact_is_verified(
|
||||
t: &TestContext,
|
||||
contact: &TestContext,
|
||||
) {
|
||||
let contact = t.add_or_lookup_contact_no_key(contact).await;
|
||||
if let Some(single_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id)
|
||||
if let Some(direct_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
assert_eq!(single_chat.blocked, Blocked::Yes);
|
||||
assert_eq!(direct_chat.blocked, Blocked::Yes);
|
||||
}
|
||||
assert!(contact.is_verified(t).await.unwrap());
|
||||
}
|
||||
@@ -4044,7 +4013,7 @@ async fn test_only_broadcast_owner_can_send_1() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
tcm.section(
|
||||
"Bob receives an answer, but shows it in a single chat because of a fingerprint mismatch",
|
||||
"Bob receives an answer, but shows it in 1:1 chat because of a fingerprint mismatch",
|
||||
);
|
||||
let rcvd = bob.recv_msg(&member_added).await;
|
||||
assert_eq!(rcvd.text, "Member bob@example.net was added.");
|
||||
@@ -4560,9 +4529,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
|
||||
assert_eq!(media.first().unwrap(), &instance1_id);
|
||||
assert_eq!(media.get(1).unwrap(), &instance2_id);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
|
||||
// add a status update for the other instance; that resorts the list
|
||||
// add a status update for the oder instance; that resorts the list
|
||||
alice
|
||||
.send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#)
|
||||
.await?;
|
||||
@@ -4631,7 +4598,7 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
sync(alice0, alice1).await;
|
||||
assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not);
|
||||
|
||||
// Unblocking a single chat doesn't unblock the contact currently.
|
||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||
Contact::unblock(alice0, a0b_contact_id).await?;
|
||||
|
||||
assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked());
|
||||
@@ -4978,6 +4945,10 @@ async fn test_sync_broadcast_and_send_message() -> Result<()> {
|
||||
vec![a2b_contact_id]
|
||||
);
|
||||
|
||||
// alice2's smeared clock may be behind alice1's one, so we need to work around "hi" appearing
|
||||
// before "You joined the channel." for bob. alice1 makes 3 more calls of
|
||||
// create_smeared_timestamp() than alice2 does as of 2026-03-10.
|
||||
SystemTime::shift(Duration::from_secs(3));
|
||||
tcm.section("Alice's second device sends a message to the channel");
|
||||
let sent_msg = alice2.send_text(a2_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -5128,126 +5099,6 @@ async fn test_broadcast_contacts_are_hidden() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_bob_cant_join_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
// The observing device has Bob blocked from the early start.
|
||||
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
|
||||
Contact::block(alice2, alice2_bob_id).await?;
|
||||
|
||||
let alice1_chat_id = create_group(alice1, "").await?;
|
||||
sync(alice1, alice2).await;
|
||||
let alice1_chat = Chat::load_from_db(alice1, alice1_chat_id).await?;
|
||||
let (alice2_chat_id, _blocked) = get_chat_id_by_grpid(alice2, &alice1_chat.grpid)
|
||||
.await?
|
||||
.unwrap();
|
||||
let qr = get_securejoin_qr(alice1, Some(alice1_chat_id)).await?;
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
|
||||
.await;
|
||||
let alice1_bob_id = alice1.add_or_lookup_contact_id(bob).await;
|
||||
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
|
||||
// "vg-member-added" from alice1 adds bob for alice2 to provide membership consistency on
|
||||
// devices.
|
||||
assert_eq!(get_chat_contacts(alice2, alice2_chat_id).await?.len(), 2);
|
||||
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
|
||||
bob.recv_msg(&alice1.pop_sent_msg().await).await;
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
// Bob can join again if he isn't blocked.
|
||||
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
|
||||
Contact::block(alice1, alice1_bob_id).await?;
|
||||
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
|
||||
bob.recv_msg(&alice1.pop_sent_msg().await).await;
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
let members = get_chat_contacts(alice1, alice1_chat_id).await?;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert!(members.contains(&ContactId::SELF));
|
||||
let past_members = get_past_chat_contacts(alice1, alice1_chat_id).await?;
|
||||
assert_eq!(past_members.len(), 1);
|
||||
assert!(past_members.contains(&alice1_bob_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_bob_cant_create_single_chat_via_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
// The observing device has Bob blocked.
|
||||
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
|
||||
Contact::block(alice2, alice2_bob_id).await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice1, None).await?;
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
let chat_cnt = get_chat_cnt(alice1).await?;
|
||||
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
|
||||
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
|
||||
.await;
|
||||
assert_eq!(get_chat_cnt(alice1).await?, chat_cnt + 1);
|
||||
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test:
|
||||
///
|
||||
/// Pinning a chat that didn't complete securejoin on one device
|
||||
/// should not cause a blank chat to be displayed on other device.
|
||||
/// Such chat should be marked as blocked until the first message is sent.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_no_blank_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.set_config_bool(Config::BccSelf, true).await?;
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(bob, None).await?;
|
||||
|
||||
// Scan QR on alice1 and pin the resulting chat.
|
||||
let chat_id = tcm.exec_securejoin_qr(alice1, bob, &qr).await;
|
||||
chat_id
|
||||
.set_visibility(&alice1.ctx, ChatVisibility::Pinned)
|
||||
.await?;
|
||||
|
||||
// alice2 should receive a sync item about changing visibility of a chat it doesn't know about.
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
// Chat created on alice2 should be blocked (hidden)
|
||||
let chat = Chat::load_from_db(&alice2.ctx, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Yes);
|
||||
assert_eq!(chat.get_name(), "");
|
||||
assert_eq!(chat.visibility, ChatVisibility::Pinned);
|
||||
|
||||
// First message should unblock and fill missing data on other device.
|
||||
let msg = alice1.send_text(chat_id, "meow").await;
|
||||
|
||||
alice2.recv_msg(&msg).await;
|
||||
let chat = Chat::load_from_db(&alice2.ctx, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
assert_eq!(chat.get_name(), "bob@example.net");
|
||||
assert_eq!(chat.visibility, ChatVisibility::Pinned);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending JPEG image with .png extension.
|
||||
///
|
||||
/// This is a regression test, previously sending failed
|
||||
@@ -5441,12 +5292,7 @@ async fn test_info_contact_id() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
alice_chat_id
|
||||
.set_ephemeral_timer(
|
||||
alice,
|
||||
Timer::Enabled {
|
||||
duration: NonZero::new(60).unwrap(),
|
||||
},
|
||||
)
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
@@ -5863,7 +5709,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_single_chat_no_group_member_timestamps() {
|
||||
async fn test_one_to_one_chat_no_group_member_timestamps() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
@@ -5913,35 +5759,6 @@ async fn test_send_edit_request() -> Result<()> {
|
||||
let forwarded = alice2.get_last_msg().await;
|
||||
assert!(!forwarded.is_edited());
|
||||
|
||||
// If a message is too long after editing, it becomes an HTML message on the receiver side. On
|
||||
// the sender side it's still text so that it can be edited again.
|
||||
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
|
||||
static REPEAT_CNT: usize = constants::DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
|
||||
let long_txt = REPEAT_TXT.repeat(REPEAT_CNT);
|
||||
send_edit_request(alice, alice_msg.id, long_txt.clone()).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let test = Message::load_from_db(alice, alice_msg.id).await?;
|
||||
assert!(!test.has_html());
|
||||
assert_eq!(test.text, long_txt);
|
||||
bob.recv_msg_opt(&sent).await;
|
||||
let test = Message::load_from_db(bob, bob_msg.id).await?;
|
||||
assert!(test.is_edited());
|
||||
assert!(test.has_html());
|
||||
let html = test.id.get_html(bob).await?.unwrap();
|
||||
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
|
||||
assert!(test.text.matches("just repeated.").count() > 0);
|
||||
|
||||
// Alice shortens the message back so it's not HTML for Bob anymore.
|
||||
send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let test = Message::load_from_db(alice, alice_msg.id).await?;
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
bob.recv_msg_opt(&sent).await;
|
||||
let test = Message::load_from_db(bob, bob_msg.id).await?;
|
||||
assert!(test.is_edited());
|
||||
assert!(!test.has_html());
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6254,9 +6071,9 @@ async fn test_forward_msgs_2ctx_missing_blob() -> Result<()> {
|
||||
|
||||
/// Tests that in multi-device setup
|
||||
/// second device learns the key of a contact
|
||||
/// via Autocrypt-Gossip in single chats.
|
||||
/// via Autocrypt-Gossip in 1:1 chats.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_single_gossip() -> Result<()> {
|
||||
async fn test_oneone_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
@@ -6605,34 +6422,3 @@ async fn test_unpromoted_group_start_message() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that outer To header is ignored for broadcast messages.
|
||||
///
|
||||
/// Broadcast messages have no recipients in the To field,
|
||||
/// but this does not mean that outer To field should be used.
|
||||
///
|
||||
/// With RFC 9788 header protection all outer headers should be ignored.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_message_replaced_to() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_broadcast_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_broadcast_id))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
let mut sent = alice.send_text(alice_broadcast_id, "Hello!").await;
|
||||
sent.payload = sent
|
||||
.payload
|
||||
.replace("To: ", "To: mallory@example.org\r\nX-Foobar: ");
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
|
||||
// The message should be assigned to the broadcast chat
|
||||
// and not to some ad hoc group with mallory@example.org
|
||||
assert_eq!(bob_msg.chat_id, bob_chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+69
-55
@@ -115,40 +115,35 @@ impl Chatlist {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
macro_rules! last_visible_msg_id_in {
|
||||
($chat_id:expr) => {
|
||||
concat!(
|
||||
"
|
||||
(SELECT id FROM msgs WHERE
|
||||
-- state=`OutDraft`.
|
||||
state=19 AND hidden=1 AND chat_id=",
|
||||
$chat_id,
|
||||
" OR
|
||||
-- `InFresh`...`OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN (10,13,16,20,24,26) AND hidden=0 AND chat_id=",
|
||||
$chat_id,
|
||||
"
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
)
|
||||
};
|
||||
}
|
||||
// The list starts with the newest chats.
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
// which would refer the outer select and take a lot of time
|
||||
// - `GROUP BY` is needed several messages may have the same
|
||||
// timestamp
|
||||
// - the list starts with the newest chats
|
||||
//
|
||||
// The query shows messages from blocked contacts in
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map_vec(concat!(
|
||||
context.sql.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp)
|
||||
ORDER BY c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(query_contact_id, ChatVisibility::Pinned),
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
@@ -159,21 +154,22 @@ ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
concat!(
|
||||
"
|
||||
SELECT c.id, m.id
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=",
|
||||
last_visible_msg_id_in!("c.id"),
|
||||
"
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"
|
||||
),
|
||||
(),
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
)
|
||||
.await?
|
||||
@@ -192,18 +188,24 @@ ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
let str_like_cmd = format!("%{}%", query.to_lowercase());
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(concat!(
|
||||
.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked!=1
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?
|
||||
AND (NOT ? OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ? AND hidden=0))
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
)
|
||||
.await?
|
||||
@@ -235,19 +237,25 @@ ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
};
|
||||
context.sql.query_map(concat!(
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
skip_id, ChatVisibility::Archived,
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
sort_id_up, ChatVisibility::Pinned,
|
||||
),
|
||||
@@ -256,17 +264,23 @@ ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
).await?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
context.sql.query_map_vec(concat!(
|
||||
context.sql.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
};
|
||||
@@ -653,7 +667,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// receive a single chat message
|
||||
// receive a one-to-one-message
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
@@ -681,12 +695,12 @@ mod tests {
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Authname");
|
||||
|
||||
// check, the single chat can be found using chatlist search query
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the single chat
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
@@ -696,7 +710,7 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// revert contact to authname, this again changes the name of the single chat
|
||||
// revert contact to authname, this again changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
@@ -714,7 +728,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// receive a single chat message without authname set
|
||||
// receive a one-to-one-message without authname set
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
@@ -737,12 +751,12 @@ mod tests {
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "bob@example.org");
|
||||
|
||||
// check, the single chat can be found using chatlist search query
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the single chat
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
@@ -753,7 +767,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat_id);
|
||||
|
||||
// revert name change, this again changes the name of the single chat to the email-address
|
||||
// revert name change, this again changes the name of the one-to-one-chat to the email-address
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
|
||||
+38
-20
@@ -17,9 +17,10 @@ use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::Provider;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{get_abs_path, time};
|
||||
use crate::transport::{add_pseudo_transport, send_sync_transports};
|
||||
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
@@ -117,6 +118,15 @@ pub enum Config {
|
||||
/// SMTP server security (e.g. TLS, STARTTLS).
|
||||
SendSecurity,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Whether to use OAuth 2.
|
||||
///
|
||||
/// Historically contained other bitflags, which are now deprecated.
|
||||
/// Should not be extended in the future, create new config keys instead.
|
||||
ServerFlags,
|
||||
|
||||
/// True if proxy is enabled.
|
||||
///
|
||||
/// Can be used to disable proxy without erasing known URLs.
|
||||
@@ -288,24 +298,27 @@ pub enum Config {
|
||||
/// Configured SMTP server password.
|
||||
ConfiguredSendPw,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Whether OAuth 2 is used with configured provider.
|
||||
ConfiguredServerFlags,
|
||||
|
||||
/// Configured folder for incoming messages.
|
||||
ConfiguredInboxFolder,
|
||||
|
||||
/// Unix timestamp of the last successful configuration.
|
||||
ConfiguredTimestamp,
|
||||
|
||||
/// ID of the configured provider from the provider database.
|
||||
ConfiguredProvider,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use [`Context::is_configured()`] instead.
|
||||
///
|
||||
/// True if account is configured.
|
||||
Configured,
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// Most usages in UIs can be replaced by `force_encryption`.
|
||||
///
|
||||
/// True if account is a chatmail account.
|
||||
IsChatmail,
|
||||
|
||||
@@ -346,21 +359,9 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// Timestamp of the last time accumulated broadcast channel reactions were sent
|
||||
LastReactionsBroadcast,
|
||||
|
||||
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
||||
LastCantDecryptOutgoingMsgs,
|
||||
|
||||
/// Timestamp of the last time automatic relay management was run
|
||||
LastAutomaticRelayManagement,
|
||||
|
||||
/// Whether to automatically add/remove transports
|
||||
AutomaticRelayManagement,
|
||||
|
||||
/// Whether automatic relay management successfully added the desired number of relays
|
||||
AutomaticRelayManagementFinished,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
@@ -454,6 +455,12 @@ pub enum Config {
|
||||
/// Return an error from `receive_imf_inner()`. For tests.
|
||||
SimulateReceiveImfError,
|
||||
|
||||
/// Enable composing emails with Header Protection as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
|
||||
/// Protected Email".
|
||||
#[strum(props(default = "1"))]
|
||||
StdHeaderProtectionComposing,
|
||||
|
||||
/// Who can call me.
|
||||
///
|
||||
/// The options are from the `WhoCanCallMe` enum.
|
||||
@@ -622,6 +629,16 @@ impl Context {
|
||||
self.get_config_bool(Config::MdnsEnabled).await
|
||||
}
|
||||
|
||||
/// Gets the configured provider.
|
||||
///
|
||||
/// The provider is determined by the current primary transport.
|
||||
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
|
||||
let provider = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.and_then(|(_transport_id, param)| param.provider);
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
@@ -804,12 +821,13 @@ impl Context {
|
||||
"Failed to update add_timestamp for the new primary transport",
|
||||
)?;
|
||||
|
||||
// Clean up SMTP queue.
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
transaction.execute("DELETE FROM imap_send", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
+215
-305
@@ -1,9 +1,11 @@
|
||||
//! # Email accounts autoconfiguration process.
|
||||
//!
|
||||
//! The module provides automatic lookup of configuration for email providers
|
||||
//! using [Mozilla Thunderbird Autoconfiguration protocol]
|
||||
//! The module provides automatic lookup of configuration
|
||||
//! for email providers based on the built-in [provider database],
|
||||
//! [Mozilla Thunderbird Autoconfiguration protocol]
|
||||
//! and [Outlook's Autodiscover].
|
||||
//!
|
||||
//! [provider database]: crate::provider
|
||||
//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
|
||||
//! [Outlook's Autodiscover]: auto_outlook
|
||||
|
||||
@@ -28,22 +30,24 @@ use crate::imap::Imap;
|
||||
use crate::log::warn;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{EnteredCertificateChecks, TransportListEntry};
|
||||
use crate::message::Message;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{self, Protocol, Socket};
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::Nosync;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::transport::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, send_sync_transports,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
|
||||
/// Maximum number of relays.
|
||||
///
|
||||
/// See <https://github.com/chatmail/core/issues/7608>.
|
||||
pub(crate) const MAX_RELAYS: usize = 5;
|
||||
/// Maximum number of relays
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
@@ -211,9 +215,8 @@ impl Context {
|
||||
self.sql.count("SELECT COUNT(*) FROM transports", ()).await
|
||||
}
|
||||
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used internally and by the automated tests.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
let removed_transport_id = self
|
||||
@@ -264,20 +267,15 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
/// UIs should call this function when the user clicks on "Remove".
|
||||
/// Core will keep listening on this transport for some time,
|
||||
/// and automatically remove it once it is no longer needed.
|
||||
///
|
||||
/// Unpublished transports are not advertised to contacts,
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// `UNPUBLISHED_TRANSPORT_KEEP_TIME` are automatically removed.
|
||||
/// The default is false, but when the user updates from a version that didn't have this flag,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
|
||||
self.sql
|
||||
.transaction(|trans| {
|
||||
@@ -309,85 +307,90 @@ impl Context {
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
if !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if old_addr.is_some()
|
||||
&& !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
&& self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
self.try_make_space_for_new_relay().await?;
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
}
|
||||
|
||||
let skip_network = false;
|
||||
if let Err(error) = configure(self, param, skip_network).await {
|
||||
// Log entered and actual params
|
||||
let configured_param = get_configured_param(self, param, skip_network).await;
|
||||
warn!(
|
||||
self,
|
||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||
param.to_string(),
|
||||
configured_param
|
||||
.map(|param| param.to_string())
|
||||
.unwrap_or("error".to_owned())
|
||||
);
|
||||
return Err(error);
|
||||
let provider = match configure(self, param).await {
|
||||
Err(error) => {
|
||||
// Log entered and actual params
|
||||
let configured_param = get_configured_param(self, param).await;
|
||||
warn!(
|
||||
self,
|
||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||
param.to_string(),
|
||||
configured_param
|
||||
.map(|param| param.to_string())
|
||||
.unwrap_or("error".to_owned())
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(provider) => provider,
|
||||
};
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
if provider::legacy_settings_for_addr(¶m.addr)?.worse_media_quality
|
||||
&& !self.config_exists(Config::MediaQuality).await?
|
||||
{
|
||||
self.set_config_ex(Nosync, Config::MediaQuality, Some("1"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is called before adding a new relay.
|
||||
/// If the maximum number of relays ([`MAX_RELAYS`]) is already reached,
|
||||
/// then it tries to make space by removing an unpublished relay.
|
||||
/// If there are multiple unpublished relays,
|
||||
/// the one that hasn't received a message for longest is removed.
|
||||
/// If there are no unpublished relays, an error is returned.
|
||||
///
|
||||
/// Note that eviction happens before we know that a new relay works,
|
||||
/// which is a trade-off we made in favor of implementation complexity.
|
||||
async fn try_make_space_for_new_relay(&self) -> Result<()> {
|
||||
if self.count_transports().await? >= MAX_RELAYS {
|
||||
// Try to automatically remove the unpublished transport that wasn't used for the longest time:
|
||||
if let Some(addr) = self
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
"SELECT addr FROM transports WHERE is_published=0
|
||||
ORDER BY last_rcvd_timestamp, add_timestamp LIMIT 1",
|
||||
(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
self,
|
||||
"Auto-deleting relay {addr} to make space for new relay."
|
||||
);
|
||||
self.delete_transport(&addr).await?;
|
||||
}
|
||||
|
||||
if self.count_transports().await? >= MAX_RELAYS {
|
||||
// Apparently, all the transports are published
|
||||
bail!("You have reached the maximum number of relays ({MAX_RELAYS})");
|
||||
}
|
||||
};
|
||||
on_configure_completed(self, provider).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves data from autoconfig
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
provider: Option<&'static Provider>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = provider {
|
||||
if let Some(config_defaults) = provider.config_defaults {
|
||||
for def in config_defaults {
|
||||
if !context.config_exists(def.key).await? {
|
||||
info!(context, "apply config_defaults {}={}", def.key, def.value);
|
||||
context
|
||||
.set_config_ex(Nosync, def.key, Some(def.value))
|
||||
.await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new_text(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(context, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves data from autoconfig and provider database
|
||||
/// to transform user-entered login parameters into complete configuration.
|
||||
async fn get_configured_param(
|
||||
ctx: &Context,
|
||||
param: &EnteredLoginParam,
|
||||
skip_network: bool,
|
||||
) -> Result<ConfiguredLoginParam> {
|
||||
ensure!(!param.addr.is_empty(), "Missing email address.");
|
||||
|
||||
@@ -400,14 +403,33 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let addr = param.addr.clone();
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await?
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
info!(ctx, "Authorized address is {}", oauth2_addr);
|
||||
addr = oauth2_addr;
|
||||
ctx.sql
|
||||
.set_raw_config("addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
}
|
||||
progress!(ctx, 20);
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
|
||||
progress!(ctx, 200);
|
||||
|
||||
let param_autoconfig = if param.imap.server.is_empty()
|
||||
let provider;
|
||||
let param_autoconfig;
|
||||
if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
&& param.imap.security == Socket::Automatic
|
||||
&& param.imap.user.is_empty()
|
||||
@@ -415,17 +437,52 @@ async fn get_configured_param(
|
||||
&& param.smtp.port == 0
|
||||
&& param.smtp.security == Socket::Automatic
|
||||
&& param.smtp.user.is_empty()
|
||||
&& !skip_network
|
||||
{
|
||||
// No advanced parameters entered by the user:
|
||||
// do Autoconfig unless the domain has hard-coded legacy servers.
|
||||
match provider::legacy_settings_for_addr(¶m.addr)?.autoconfig_servers {
|
||||
Some(servers) => Some(servers),
|
||||
None => get_autoconfig(ctx, param, ¶m_domain).await,
|
||||
// no advanced parameters entered by the user: query provider-database or do Autoconfig
|
||||
info!(
|
||||
ctx,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
provider = provider::get_provider_info(¶m_domain);
|
||||
if let Some(provider) = provider {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "Offline autoconfig found, but no servers defined.");
|
||||
param_autoconfig = None;
|
||||
} else {
|
||||
info!(ctx, "Offline autoconfig found.");
|
||||
let servers = provider
|
||||
.server
|
||||
.iter()
|
||||
.map(|s| ServerParams {
|
||||
protocol: s.protocol,
|
||||
socket: s.socket,
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::Email => param.addr.to_string(),
|
||||
UsernamePattern::Emaillocalpart => {
|
||||
if let Some(at) = param.addr.find('@') {
|
||||
param.addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
param.addr.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
param_autoconfig = Some(servers)
|
||||
}
|
||||
} else {
|
||||
// Try receiving autoconfig
|
||||
info!(ctx, "No offline autoconfig found.");
|
||||
param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
provider = None;
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
progress!(ctx, 500);
|
||||
|
||||
@@ -504,6 +561,7 @@ async fn get_configured_param(
|
||||
.collect(),
|
||||
smtp_user: param.smtp.user.clone(),
|
||||
smtp_password,
|
||||
provider,
|
||||
certificate_checks: match param.certificate_checks {
|
||||
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
|
||||
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
|
||||
@@ -512,88 +570,83 @@ async fn get_configured_param(
|
||||
ConfiguredCertificateChecks::AcceptInvalidCertificates
|
||||
}
|
||||
},
|
||||
oauth2: param.oauth2,
|
||||
};
|
||||
Ok(configured_login_param)
|
||||
}
|
||||
|
||||
pub(crate) async fn configure(
|
||||
ctx: &Context,
|
||||
param: &EnteredLoginParam,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
let configured_param = get_configured_param(ctx, param, skip_network).await?;
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some())?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
if !skip_network {
|
||||
// Spawn SMTP configuration task
|
||||
// to try SMTP while connecting to IMAP.
|
||||
let context_smtp = ctx.clone();
|
||||
let smtp_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
// Spawn SMTP configuration task
|
||||
// to try SMTP while connecting to IMAP.
|
||||
let context_smtp = ctx.clone();
|
||||
let smtp_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
|
||||
let proxy_config2 = proxy_config.clone();
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
smtp.connect(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&proxy_config2,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
)
|
||||
.await?;
|
||||
let proxy_config2 = proxy_config.clone();
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
smtp.connect(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&proxy_config2,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
// Configure IMAP
|
||||
|
||||
let transport_id = 0;
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let configuring = true;
|
||||
let imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(imap_session) => imap_session,
|
||||
Err(err) => {
|
||||
bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
|
||||
}
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await??;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
let transport_id = 0;
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let configuring = true;
|
||||
let imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(imap_session) => imap_session,
|
||||
Err(err) => {
|
||||
bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
|
||||
}
|
||||
};
|
||||
|
||||
// Drop the imap connection explicitly
|
||||
// to make sure that it's not forgotten in a future refactoring
|
||||
drop(imap_session);
|
||||
drop(imap);
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await??;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(imap_session);
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
let provider = configured_param.provider;
|
||||
configured_param
|
||||
.clone()
|
||||
.save_to_transports_table(ctx, param, time())
|
||||
@@ -615,7 +668,7 @@ pub(crate) async fn configure(
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.emit_event(EventType::AccountsItemChanged);
|
||||
|
||||
Ok(())
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
@@ -653,12 +706,12 @@ async fn get_autoconfig(
|
||||
}
|
||||
progress!(ctx, 300);
|
||||
|
||||
// `?emailaddress=` query string is excluded on purpose.
|
||||
// It is not part of the URL according to <https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/06/>.
|
||||
// Related discussion confirming this is at <https://github.com/benbucksch/autoconfig-spec/issues/17>.
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!("https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml"),
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
|
||||
&format!(
|
||||
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
|
||||
),
|
||||
¶m.addr,
|
||||
accept_invalid_certificates,
|
||||
)
|
||||
@@ -742,14 +795,10 @@ pub enum Error {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredImapLoginParam;
|
||||
use crate::sql::update_transport_last_rcvd_timestamp;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::transport::add_pseudo_transport;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_panic_on_bad_credentials() {
|
||||
@@ -775,148 +824,9 @@ mod tests {
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
let skip_network = false;
|
||||
let configured_param = get_configured_param(t, &entered_param, skip_network).await?;
|
||||
let configured_param = get_configured_param(t, &entered_param).await?;
|
||||
assert_eq!(configured_param.imap_user, "alice@example.net");
|
||||
assert_eq!(configured_param.smtp_user, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_make_place_for_new_relay() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Setting ConfiguredAddr on an unconfigured account creates a pseudo primary transport
|
||||
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
|
||||
.await?;
|
||||
|
||||
// Test that try_make_place_for_new_relay() doesn't do anything when we're below the limit
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
|
||||
for i in 0..(MAX_RELAYS - 2) {
|
||||
add_pseudo_transport(&t, &format!("transport{i}@example.org")).await?;
|
||||
}
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
|
||||
// Test that try_make_place_for_new_relay() removes the unpublished transport
|
||||
// when we're at the limit
|
||||
add_pseudo_transport(&t, "unpublished@example.org").await?;
|
||||
t.set_transport_unpublished("unpublished@example.org", true)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("unpublished@example.org",),
|
||||
)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
|
||||
// Test that if there are multiple unpublished relays,
|
||||
// the one that was used least recently is removed
|
||||
t.set_transport_unpublished("transport0@example.org", true)
|
||||
.await?;
|
||||
add_pseudo_transport(&t, "other_unpublished@example.org").await?;
|
||||
t.set_transport_unpublished("other_unpublished@example.org", true)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
|
||||
let transport0_id: u32 = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM transports WHERE addr=?",
|
||||
("transport0@example.org",),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let other_unpublished_id: u32 = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM transports WHERE addr=?",
|
||||
("other_unpublished@example.org",),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
update_transport_last_rcvd_timestamp(&t, transport0_id).await?;
|
||||
SystemTime::shift(std::time::Duration::from_secs(10));
|
||||
update_transport_last_rcvd_timestamp(&t, other_unpublished_id).await?;
|
||||
|
||||
// Test that try_make_place_for_new_relay()
|
||||
// removes the relay with the oldest last_rcvd_timestamp
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("transport0@example.org",),
|
||||
)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("other_unpublished@example.org",),
|
||||
)
|
||||
.await?,
|
||||
true
|
||||
);
|
||||
|
||||
// Test that try_make_place_for_new_relay() fails
|
||||
// if there are MAX_RELAYS published transports
|
||||
add_pseudo_transport(&t, "published_extra@example.org").await?;
|
||||
t.set_transport_unpublished("other_unpublished@example.org", false)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
assert!(t.try_make_space_for_new_relay().await.is_err());
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if Alice adds maximum number of transports,
|
||||
/// Bob sends messages to all of them.
|
||||
///
|
||||
/// This way we don't need to care about the order
|
||||
/// of addresses advertised in the public key.
|
||||
/// Previously the number of addresses
|
||||
/// taken from the key was less than the maximum
|
||||
/// number of advertised addresses.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_can_send_to_max_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// One relay is added already by default.
|
||||
for i in 1..MAX_RELAYS {
|
||||
add_pseudo_transport(alice, &format!("transport{i}@example.org")).await?;
|
||||
}
|
||||
assert_eq!(alice.count_transports().await?, MAX_RELAYS);
|
||||
|
||||
let bob_chat_id = bob.create_chat_id(alice).await;
|
||||
|
||||
bob.set_config_bool(Config::BccSelf, false).await?;
|
||||
let sent = bob.send_text(bob_chat_id, "Hello!").await;
|
||||
assert_eq!(
|
||||
sent.recipients.split(' ').count(),
|
||||
MAX_RELAYS,
|
||||
"List of recipients is {}",
|
||||
sent.recipients
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use quick_xml::XmlVersion;
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
@@ -72,7 +71,7 @@ fn parse_server<B: BufRead>(
|
||||
})
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
|
||||
typ.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
@@ -102,11 +101,7 @@ fn parse_server<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event
|
||||
.xml_content(XmlVersion::Implicit1_0)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_owned();
|
||||
let val = event.xml_content().unwrap_or_default().trim().to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use quick_xml::XmlVersion;
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
@@ -80,7 +79,7 @@ fn parse_protocol<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref e) => {
|
||||
let val = e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
@@ -124,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Variable server parameters lists
|
||||
|
||||
pub use crate::provider::{Protocol, Socket};
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Set of variable parameters to try during configuration.
|
||||
///
|
||||
/// Can be loaded from online configuration
|
||||
/// Can be loaded from offline provider database, online configuration
|
||||
/// or derived from user entered parameters.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ServerParams {
|
||||
@@ -302,7 +302,8 @@ mod tests {
|
||||
// as this is very uncommon configuration
|
||||
// and not worth doubling the number of candidates to try.
|
||||
// If such configuration is used, email provider
|
||||
// should provide XML autoconfig.
|
||||
// should provide XML autoconfig or
|
||||
// be added to the provider database as an exception.
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::Imap,
|
||||
|
||||
+27
-5
@@ -99,7 +99,7 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
/// A single chat (a chat with a single contact).
|
||||
/// A 1:1 chat, i.e. a normal chat with a single contact.
|
||||
///
|
||||
/// Created by [`ChatId::create_for_contact`].
|
||||
Single = 100,
|
||||
@@ -162,6 +162,25 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
|
||||
|
||||
// Flags for configuring IMAP and SMTP servers.
|
||||
// These flags are optional
|
||||
// and may be set together with the username, password etc.
|
||||
// via dc_set_config() using the key "server_flags".
|
||||
|
||||
/// Force OAuth2 authorization.
|
||||
///
|
||||
/// This flag does not skip automatic configuration.
|
||||
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
|
||||
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
||||
|
||||
/// Force NORMAL authorization, this is the default.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
|
||||
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
|
||||
// max. weight of images to send w/o recoding
|
||||
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
|
||||
pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
@@ -180,10 +199,13 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
/// usage by UIs.
|
||||
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
|
||||
|
||||
// Fallback for the maximum number of recipients in SMTP's `RCPT TO:`;
|
||||
// recipient lists exceeding the limit are sent in chunks.
|
||||
// Relays typically advertise their limit via IMAP METADATA.
|
||||
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: u32 = 50;
|
||||
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
|
||||
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
|
||||
// `max_smtp_rcpt_to` in the provider db.
|
||||
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
|
||||
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
|
||||
|
||||
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
|
||||
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
|
||||
|
||||
+6
-2
@@ -1230,13 +1230,17 @@ ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
|
||||
.await?;
|
||||
|
||||
if let Some(query) = query {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name2 = stock_str::self_msg(context);
|
||||
|
||||
if self_addrs.iter().any(|a| a.contains(query))
|
||||
if self_addr.contains(query)
|
||||
|| self_name.contains(query)
|
||||
|| self_name2.contains(query)
|
||||
{
|
||||
@@ -1852,7 +1856,7 @@ pub(crate) async fn set_blocked(
|
||||
|
||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||
// non-destructive blocking->unblocking.
|
||||
// (Maybe, beside single chats (type=100) we should also block group chats with only this user.
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
|
||||
@@ -152,20 +152,6 @@ async fn test_get_contacts() -> Result<()> {
|
||||
assert_eq!(contacts.len(), 1);
|
||||
let contacts = Contact::get_all(&context, 0, Some("δ")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// Searching for a secondary self address finds "Me",
|
||||
// even if the transport is unpublished.
|
||||
crate::transport::add_pseudo_transport(&context, "bob@second.example").await?;
|
||||
context
|
||||
.set_transport_unpublished("bob@second.example", true)
|
||||
.await?;
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
constants::DC_GCL_ADD_SELF,
|
||||
Some("bob@second.example"),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(contacts, vec![ContactId::SELF]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -351,7 +337,7 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// first message creates contact and single-chat without name set
|
||||
// first message creates contact and one-to-one-chat without name set
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: f@example.org\n\
|
||||
|
||||
+35
-56
@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -30,6 +31,7 @@ use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{ConnectivityStore, SchedulerState};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
use crate::tools::{self, duration_to_str, time, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{chatlist_events, stats};
|
||||
@@ -225,15 +227,18 @@ pub struct InnerContext {
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) smeared_timestamp: SmearedTimestamp,
|
||||
/// The global "ongoing" process state.
|
||||
///
|
||||
/// This is a global mutex-like state for operations which should be modal in the
|
||||
/// clients.
|
||||
running_state: RwLock<RunningState>,
|
||||
/// Mutex to enforce only a single running oauth2 is running.
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
|
||||
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
|
||||
/// Mutex to prevent running housekeeping or relay management from multiple threads at once.
|
||||
pub(crate) background_task_mutex: Mutex<()>,
|
||||
/// Mutex to prevent running housekeeping from multiple threads at once.
|
||||
pub(crate) housekeeping_mutex: Mutex<()>,
|
||||
|
||||
/// Mutex to prevent multiple IMAP loops from fetching the messages at once.
|
||||
///
|
||||
@@ -263,8 +268,8 @@ pub struct InnerContext {
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc2971>
|
||||
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
|
||||
|
||||
/// IMAP METADATA, per transport id.
|
||||
pub(crate) metadata: RwLock<BTreeMap<u32, ServerMetadata>>,
|
||||
/// IMAP METADATA.
|
||||
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
///
|
||||
@@ -292,9 +297,13 @@ pub struct InnerContext {
|
||||
/// because the lock is used from synchronous [`Context::emit_event`].
|
||||
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
|
||||
|
||||
/// Push subscriber to store device token.
|
||||
/// Push subscriber to store device token
|
||||
/// and register for heartbeat notifications.
|
||||
pub(crate) push_subscriber: PushSubscriber,
|
||||
|
||||
/// True if account has subscribed to push notifications via IMAP.
|
||||
pub(crate) push_subscribed: AtomicBool,
|
||||
|
||||
/// TLS session resumption cache.
|
||||
pub(crate) tls_session_store: TlsSessionStore,
|
||||
|
||||
@@ -477,8 +486,10 @@ impl Context {
|
||||
blobdir,
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(dbfile),
|
||||
smeared_timestamp: SmearedTimestamp::new(),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
background_task_mutex: Mutex::new(()),
|
||||
housekeeping_mutex: Mutex::new(()),
|
||||
fetch_msgs_mutex: Mutex::new(()),
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
@@ -487,12 +498,13 @@ impl Context {
|
||||
quota: RwLock::new(BTreeMap::new()),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(BTreeMap::new()),
|
||||
metadata: RwLock::new(None),
|
||||
creation_time: tools::Time::now(),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
migration_error: parking_lot::RwLock::new(None),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
tls_session_store: TlsSessionStore::new(),
|
||||
spki_hash_store: SpkiHashStore::new(),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
@@ -557,35 +569,26 @@ impl Context {
|
||||
self.scheduler.maybe_network().await;
|
||||
}
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// 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 a single email can be sent to.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<u32> {
|
||||
let Some((transport_id, param)) = ConfiguredLoginParam::load(self).await? else {
|
||||
bail!("Not configured");
|
||||
};
|
||||
let metadata_limit = self
|
||||
.metadata
|
||||
.read()
|
||||
.await
|
||||
.get(&transport_id)
|
||||
.and_then(|metadata| metadata.max_smtp_rcpt_to);
|
||||
if let Some(limit) = metadata_limit {
|
||||
return Ok(limit);
|
||||
}
|
||||
if let Some(limit) =
|
||||
crate::provider::legacy_settings_for_addr(¶m.addr)?.max_smtp_rcpt_to
|
||||
{
|
||||
return Ok(limit);
|
||||
}
|
||||
Ok(constants::DEFAULT_MAX_SMTP_RCPT_TO)
|
||||
/// 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 => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
|
||||
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
|
||||
},
|
||||
usize::from,
|
||||
);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Does a single round of fetching from IMAP and returns.
|
||||
@@ -914,7 +917,7 @@ impl Context {
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
|
||||
if let Some(metadata) = self.metadata.read().await.values().next() {
|
||||
if let Some(metadata) = &*self.metadata.read().await {
|
||||
if let Some(comment) = &metadata.comment {
|
||||
res.insert("imap_server_comment", format!("{comment:?}"));
|
||||
}
|
||||
@@ -956,12 +959,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_reactions_broadcast",
|
||||
self.get_config_int(Config::LastReactionsBroadcast)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_cant_decrypt_outgoing_msgs",
|
||||
self.get_config_int(Config::LastCantDecryptOutgoingMsgs)
|
||||
@@ -1039,24 +1036,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_automatic_relay_management",
|
||||
self.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"automatic_relay_management",
|
||||
self.get_config_bool(Config::AutomaticRelayManagement)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"automatic_relay_management_finished",
|
||||
self.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
|
||||
@@ -285,6 +285,7 @@ async fn test_get_info_completeness() {
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
|
||||
+7
-29
@@ -6,16 +6,13 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use mailparse::ParsedMail;
|
||||
use pgp::composed::DecryptionOptions;
|
||||
use pgp::composed::Esk;
|
||||
use pgp::composed::Message;
|
||||
use pgp::composed::PlainSessionKey;
|
||||
use pgp::composed::SignedSecretKey;
|
||||
use pgp::composed::TheRing;
|
||||
use pgp::composed::decrypt_session_key_with_password;
|
||||
use pgp::packet::SymKeyEncryptedSessionKey;
|
||||
use pgp::types::Password;
|
||||
use pgp::types::Seipdv1ReadMode;
|
||||
use pgp::types::StringToKey;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
@@ -51,15 +48,6 @@ pub(crate) async fn decrypt(
|
||||
};
|
||||
let expected_sender_fingerprint: Option<String>;
|
||||
|
||||
let abort_early = true;
|
||||
|
||||
// Use streaming mode for SEIPDv1 decryption to save memory.
|
||||
// This was the default in rPGP 0.19.0
|
||||
// and requires explicitly changing the mode in rPGP 0.20.0.
|
||||
// SEPIDv2 is decrypted in streaming mode in any case.
|
||||
let decrypt_options =
|
||||
DecryptionOptions::new().set_seipdv1_read_mode(Seipdv1ReadMode::Streaming);
|
||||
|
||||
let plain = if let Message::Encrypted { esk, .. } = &*msg
|
||||
// We only allow one ESK for symmetrically encrypted messages
|
||||
// to avoid dealing with messages that are encrypted to multiple symmetric keys
|
||||
@@ -73,15 +61,9 @@ pub(crate) async fn decrypt(
|
||||
expected_sender_fingerprint = fingerprint;
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<Message<'_>> {
|
||||
let ring = TheRing {
|
||||
session_keys: vec![psk],
|
||||
decrypt_options,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (plain, _ring_result) = msg
|
||||
.decrypt_the_ring(ring, abort_early)
|
||||
.context("decrypt_the_ring")?;
|
||||
let plain = msg
|
||||
.decrypt_with_session_key(psk)
|
||||
.context("decrypt_with_session_key")?;
|
||||
|
||||
let plain: Message<'static> = plain.decompress()?;
|
||||
Ok(plain)
|
||||
@@ -93,15 +75,11 @@ pub(crate) async fn decrypt(
|
||||
expected_sender_fingerprint = None;
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<Message<'_>> {
|
||||
let empty_pw = Password::empty();
|
||||
let secret_keys: Vec<&SignedSecretKey> = secret_keys.iter().collect();
|
||||
let ring = TheRing {
|
||||
secret_keys,
|
||||
decrypt_options,
|
||||
..Default::default()
|
||||
};
|
||||
let (plain, _ring_result) = msg
|
||||
.decrypt_the_ring(ring, abort_early)
|
||||
.context("decrypt_the_ring")?;
|
||||
let plain = msg
|
||||
.decrypt_with_keys(vec![&empty_pw], secret_keys)
|
||||
.context("decrypt_with_keys")?;
|
||||
|
||||
let plain: Message<'static> = plain.decompress()?;
|
||||
Ok(plain)
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ use std::io::BufRead;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use quick_xml::{
|
||||
Reader, XmlVersion,
|
||||
Reader,
|
||||
errors::Error as QuickXmlError,
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
};
|
||||
@@ -327,7 +327,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
})
|
||||
{
|
||||
let href = href
|
||||
.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
@@ -374,7 +374,7 @@ fn maybe_push_tag(
|
||||
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
|
||||
event.attributes().any(|r| {
|
||||
r.map(|a| {
|
||||
a.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
|
||||
a.decode_and_unescape_value(reader.decoder())
|
||||
.map(|v| v == name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user