mirror of
https://github.com/chatmail/core.git
synced 2026-04-13 03:30:29 +03:00
Compare commits
107 Commits
feat_encry
...
1.103.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6f5fd139 | ||
|
|
519f658c07 | ||
|
|
f5cb56fd86 | ||
|
|
c830db07ad | ||
|
|
9093702692 | ||
|
|
edd58b4b7a | ||
|
|
72432d65ba | ||
|
|
eb611a2855 | ||
|
|
8aa73ed6ae | ||
|
|
1224222984 | ||
|
|
3360c6aa96 | ||
|
|
bfddd3fc69 | ||
|
|
35cd81a75f | ||
|
|
f11fceb63a | ||
|
|
f14a28db54 | ||
|
|
cacc01bac0 | ||
|
|
fc386f4fa1 | ||
|
|
3743aaa16e | ||
|
|
22b4640b31 | ||
|
|
1dcda4989d | ||
|
|
e59768167a | ||
|
|
7e5becb5e5 | ||
|
|
b4f348ccea | ||
|
|
7a0dd24681 | ||
|
|
e5ae82252f | ||
|
|
49c45d1007 | ||
|
|
5502bff986 | ||
|
|
ee19789cac | ||
|
|
bad5a1de55 | ||
|
|
3cdbe213a3 | ||
|
|
3b5b1bf877 | ||
|
|
bcd9229ffe | ||
|
|
4df588668a | ||
|
|
de96500c1a | ||
|
|
9b881cdd19 | ||
|
|
2ccf39800d | ||
|
|
5a3065344e | ||
|
|
98b6b5e3f6 | ||
|
|
4d81fa6df5 | ||
|
|
8e8582e953 | ||
|
|
7f4c05e88f | ||
|
|
20e63659a1 | ||
|
|
c5af69db2b | ||
|
|
030241d1c3 | ||
|
|
4f01c43a93 | ||
|
|
ab94471e91 | ||
|
|
140aa68811 | ||
|
|
d9ef38e370 | ||
|
|
acf66116cd | ||
|
|
8e69125128 | ||
|
|
7402ca3568 | ||
|
|
d7b240f25c | ||
|
|
e5e4d3bed4 | ||
|
|
1a59302749 | ||
|
|
f0a5bbedb4 | ||
|
|
139665b3d6 | ||
|
|
ca23d59148 | ||
|
|
1635f00a62 | ||
|
|
29a4404257 | ||
|
|
24db29f526 | ||
|
|
e27b64274f | ||
|
|
46721bcf46 | ||
|
|
80cefdd1d5 | ||
|
|
9972f4da48 | ||
|
|
1ab5401501 | ||
|
|
0b60cc8341 | ||
|
|
85b4746516 | ||
|
|
d17ac9c83c | ||
|
|
e6ff513aac | ||
|
|
ffbfeab977 | ||
|
|
09db062958 | ||
|
|
ab7732d9ae | ||
|
|
46594ec707 | ||
|
|
18426561e3 | ||
|
|
aeb7e3a9e1 | ||
|
|
a77c46be20 | ||
|
|
a4be2cddf2 | ||
|
|
240b335181 | ||
|
|
53d6807e8d | ||
|
|
db38cc8891 | ||
|
|
4e2aeceeec | ||
|
|
9b04a04568 | ||
|
|
f2c97bda66 | ||
|
|
6c4d919828 | ||
|
|
f6a502a8e3 | ||
|
|
e5be023e4b | ||
|
|
47743bdcfa | ||
|
|
b0962363c2 | ||
|
|
8855ef72bc | ||
|
|
264727a414 | ||
|
|
98c16ddc4d | ||
|
|
c7691fbebe | ||
|
|
62f92d5b28 | ||
|
|
2ae9165bfb | ||
|
|
08de326930 | ||
|
|
b341cfd4d9 | ||
|
|
a76b018900 | ||
|
|
9783da5d8e | ||
|
|
6f0985dcaa | ||
|
|
0b44886b62 | ||
|
|
36991b5c8a | ||
|
|
afb7f89722 | ||
|
|
0248a36561 | ||
|
|
2e7470115c | ||
|
|
06b376d242 | ||
|
|
4b17813b9f | ||
|
|
960a7f82ef |
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
@@ -26,15 +26,12 @@ jobs:
|
||||
- run: rustup component add rustfmt
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
@@ -54,7 +51,7 @@ jobs:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -65,10 +62,7 @@ jobs:
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- name: Rustdoc
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --document-private-items --no-deps
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
@@ -77,19 +71,19 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.65.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.65.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.57.0
|
||||
# Minimum Supported Rust Version = 1.61.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.57.0
|
||||
rust: 1.61.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -105,21 +99,13 @@ jobs:
|
||||
uses: swatinem/rust-cache@v1
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl --benches
|
||||
run: cargo check --all --bins --examples --tests --features repl --benches
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
run: cargo test --all
|
||||
|
||||
- name: test cargo vendor
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: vendor
|
||||
run: cargo vendor
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
@@ -133,10 +119,7 @@ jobs:
|
||||
|
||||
- name: build C library
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p deltachat_ffi --features jsonrpc
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: run python tests
|
||||
if: ${{ matrix.python }}
|
||||
@@ -147,6 +130,21 @@ jobs:
|
||||
working-directory: python
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
- name: build deltachat-rpc-server
|
||||
if: ${{ matrix.python }}
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: add deltachat-rpc-server to path
|
||||
if: ${{ matrix.python }}
|
||||
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
||||
|
||||
- name: run deltachat-rpc-client tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e py3
|
||||
|
||||
- name: install pypy
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
|
||||
@@ -15,8 +15,8 @@ jobs:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: get tag
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build:tsc
|
||||
npm run build
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- uses: actions-rs/toolchain@v1
|
||||
|
||||
2
.github/workflows/node-delete-preview.yml
vendored
2
.github/workflows/node-delete-preview.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
id: getid
|
||||
run: |
|
||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
||||
echo ::set-output name=prid::$PULLREQUEST_ID
|
||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
||||
- name: Renaming
|
||||
run: |
|
||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
||||
|
||||
4
.github/workflows/node-docs.yml
vendored
4
.github/workflows/node-docs.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
|
||||
14
.github/workflows/node-package.yml
vendored
14
.github/workflows/node-package.yml
vendored
@@ -16,8 +16,8 @@ jobs:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
|
||||
8
.github/workflows/node-tests.yml
vendored
8
.github/workflows/node-tests.yml
vendored
@@ -16,8 +16,8 @@ jobs:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
|
||||
9
.github/workflows/repl.yml
vendored
9
.github/workflows/repl.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -20,13 +20,10 @@ jobs:
|
||||
override: true
|
||||
|
||||
- name: build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --example repl --features repl,vendored
|
||||
run: cargo build --example repl --features repl,vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: repl.exe
|
||||
path: 'target/debug/examples/repl.exe'
|
||||
|
||||
2
.github/workflows/upload-docs.yml
vendored
2
.github/workflows/upload-docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,8 +12,8 @@ include
|
||||
*.db
|
||||
*.db-blobs
|
||||
|
||||
.tox
|
||||
python/.eggs
|
||||
python/.tox
|
||||
*.egg-info
|
||||
__pycache__
|
||||
python/src/deltachat/capi*.so
|
||||
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -6,9 +6,57 @@
|
||||
|
||||
### API-Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
## 1.103.0
|
||||
|
||||
### Changes
|
||||
- Disable Autocrypt & Authres-checking for mailing lists,
|
||||
because they don't work well with mailing lists #3765
|
||||
- Refactor: Remove the remaining AsRef<str> #3669
|
||||
- Add more logging to `fetch_many_msgs` and refactor it #3811
|
||||
- Small speedup #3780
|
||||
- Log the reason when the message cannot be sent to the chat #3810
|
||||
- Add IMAP server ID line to the context info only when it is known #3814
|
||||
- Remove autogenerated typescript files #3815
|
||||
- Move functions that require an IMAP session from `Imap` to `Session`
|
||||
to reduce the number of code paths where IMAP session may not exist.
|
||||
Drop connection on error instead of trying to disconnect,
|
||||
potentially preventing IMAP task from getting stuck. #3812
|
||||
|
||||
### API-Changes
|
||||
- Add Python API to send reactions #3762
|
||||
- jsonrpc: add message errors to MessageObject #3788
|
||||
- jsonrpc: Add async Python client #3734
|
||||
|
||||
### Fixes
|
||||
- Make sure malformed messsages will never block receiving further messages anymore #3771
|
||||
- strip leading/trailing whitespace from "Chat-Group-Name{,-Changed}:" headers content #3650
|
||||
- Assume all Thunderbird users prefer encryption #3774
|
||||
- refactor peerstate handling to ensure no duplicate peerstates #3776
|
||||
- Fetch messages in order of their INTERNALDATE (fixes reactions for Gmail f.e.) #3789
|
||||
- python: do not pass NULL to ffi.gc if the context can't be created #3818
|
||||
- Add read/write timeouts to IMAP sockets #3820
|
||||
- Add connection timeout to IMAP sockets #3828
|
||||
- Disable read timeout during IMAP IDLE #3826
|
||||
- Bots automatically accept mailing lists #3831
|
||||
|
||||
|
||||
## 1.102.0
|
||||
|
||||
### Changes
|
||||
|
||||
- If an email has multiple From addresses, handle this as if there was
|
||||
no From address, to prevent from forgery attacks. Also, improve
|
||||
handling of emails with invalid From addresses in general #3667
|
||||
|
||||
### API-Changes
|
||||
|
||||
### Fixes
|
||||
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
|
||||
- fetch messages sequentially to fix reactions on partially downloaded messages #3688
|
||||
- Fix a bug where one malformed message blocked receiving any further messages #3769
|
||||
|
||||
|
||||
## 1.101.0
|
||||
|
||||
439
Cargo.lock
generated
439
Cargo.lock
generated
@@ -23,12 +23,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.2"
|
||||
@@ -75,6 +69,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
@@ -98,9 +98,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
|
||||
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
@@ -123,7 +123,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/async-email/async-imap?branch=master#8755b666fcd8991ed4d09864b67aa88a1eb6934f"
|
||||
source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-native-tls",
|
||||
@@ -224,7 +224,7 @@ version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
@@ -242,7 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-core 0.2.9",
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
@@ -250,8 +250,8 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"itoa 1.0.3",
|
||||
"matchit",
|
||||
"itoa",
|
||||
"matchit 0.5.0",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
@@ -269,6 +269,42 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08b108ad2665fa3f6e6a517c3d80ec3e77d224c47d605167aefaa5d7ef97fa48"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core 0.3.0",
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"itoa",
|
||||
"matchit 0.7.0",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha-1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.2.9"
|
||||
@@ -285,6 +321,23 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"mime",
|
||||
"rustversion",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.66"
|
||||
@@ -295,7 +348,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.5.3",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
@@ -373,18 +426,6 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buf_redux"
|
||||
version = "0.8.4"
|
||||
@@ -435,12 +476,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
@@ -502,6 +537,33 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.3"
|
||||
@@ -514,13 +576,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
version = "3.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
||||
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -542,11 +614,11 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.2.4"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
|
||||
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
|
||||
dependencies = [
|
||||
"cache-padded",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -603,15 +675,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.3.6"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f"
|
||||
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"atty",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"csv",
|
||||
"futures",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
@@ -621,7 +694,6 @@ dependencies = [
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
@@ -631,9 +703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.4.5"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
@@ -704,28 +776,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa 0.4.8",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "3.2.0"
|
||||
@@ -815,18 +865,9 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -885,26 +926,27 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tagger",
|
||||
"tempfile",
|
||||
"textwrap 0.16.0",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-stream",
|
||||
"tokio-tar",
|
||||
"toml",
|
||||
"trust-dns-resolver",
|
||||
"url",
|
||||
"uuid 1.2.1",
|
||||
"uuid 1.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"axum",
|
||||
"axum 0.6.1",
|
||||
"deltachat",
|
||||
"env_logger 0.9.1",
|
||||
"env_logger 0.10.0",
|
||||
"futures",
|
||||
"log",
|
||||
"num-traits",
|
||||
@@ -920,11 +962,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat-jsonrpc",
|
||||
"env_logger 0.9.1",
|
||||
"env_logger 0.10.0",
|
||||
"futures-lite",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -943,7 +985,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1240,12 +1282,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
|
||||
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime 2.1.0",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
@@ -1347,7 +1389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix",
|
||||
"rustix 0.35.7",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
@@ -1370,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.5.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1622,6 +1664,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1647,7 +1698,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.3",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1696,9 +1747,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "1.1.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
|
||||
checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
@@ -1730,7 +1784,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.3",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -1794,9 +1848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.4"
|
||||
version = "0.24.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
|
||||
checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
@@ -1851,6 +1905,16 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24c3f4eff5495aee4c0399d7b6a0dc2b6e81be84242ffbfcf253ebacccc1d0cb"
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.0"
|
||||
@@ -1869,6 +1933,18 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
|
||||
dependencies = [
|
||||
"hermit-abi 0.2.6",
|
||||
"io-lifetimes 1.0.3",
|
||||
"rustix 0.36.4",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.3"
|
||||
@@ -1878,12 +1954,6 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.3"
|
||||
@@ -1892,9 +1962,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.2.6"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -1989,6 +2059,12 @@ version = "0.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
@@ -2046,6 +2122,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
@@ -2091,6 +2173,15 @@ dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.5"
|
||||
@@ -2240,11 +2331,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.1"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
|
||||
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2330,6 +2421,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
|
||||
|
||||
[[package]]
|
||||
name = "os_type"
|
||||
version = "2.4.0"
|
||||
@@ -2541,14 +2638,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.5"
|
||||
version = "0.17.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba"
|
||||
checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"deflate",
|
||||
"miniz_oxide",
|
||||
"flate2",
|
||||
"miniz_oxide 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2816,21 +2913,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.27"
|
||||
@@ -2848,9 +2939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||
checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bytes",
|
||||
@@ -2958,12 +3049,26 @@ checksum = "d51cc38aa10f6bbb377ed28197aa052aa4e2b762c22be9d3153d01822587e787"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"io-lifetimes 0.7.2",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.0.46",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.36.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes 1.0.3",
|
||||
"libc",
|
||||
"linux-raw-sys 0.1.3",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.9"
|
||||
@@ -3074,28 +3179,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.147"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.147"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
|
||||
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3104,15 +3199,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.87"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
|
||||
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
|
||||
dependencies = [
|
||||
"itoa 1.0.3",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -3120,16 +3224,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.3",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
|
||||
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -3304,9 +3408,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.103"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
|
||||
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3333,9 +3437,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tagger"
|
||||
version = "4.3.3"
|
||||
version = "4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77dd78fc7dd20ba3a13620ec231cef9e73ea5c7ba162f6c4e05b1d521e04b221"
|
||||
checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
@@ -3360,15 +3464,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
@@ -3438,9 +3533,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.21.2"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
|
||||
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -3456,6 +3551,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-io-timeout"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.8.0"
|
||||
@@ -3575,9 +3680,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
@@ -3705,9 +3810,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "typescript-type-def"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1781793d51f116db5bb614f42b42aef2f3fdebe8b2f5f7d00254ed6bb14b0c69"
|
||||
checksum = "947b91d2fe9ec02a6749b8b645541f16f527e2ea88a60b3f774eca26fd657325"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"typescript-type-def-derive",
|
||||
@@ -3715,9 +3820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typescript-type-def-derive"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ac1df09a36962cc2b9404e7fd78cc58c060ecb216c24d666de932e16ff6e539"
|
||||
checksum = "e5c1bfe689e4067733530495b04959b00f05cd95f038bed59af4fc70b3e26240"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"ident_case",
|
||||
@@ -3810,9 +3915,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
|
||||
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
"serde",
|
||||
@@ -4148,7 +4253,7 @@ dependencies = [
|
||||
"async-channel",
|
||||
"async-mutex",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum 0.5.17",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"log",
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.57"
|
||||
rust-version = "1.61"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -39,14 +39,14 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch =
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.24.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.16", optional = true }
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num_cpus = "1.14"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.16.0"
|
||||
@@ -57,7 +57,7 @@ quick-xml = "0.23"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.8"
|
||||
regex = "1.6"
|
||||
regex = "1.7"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "10", optional = true }
|
||||
@@ -74,19 +74,20 @@ toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.8"
|
||||
humansize = "1"
|
||||
humansize = "2"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.3.3"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.0"
|
||||
async-channel = "1.6.1"
|
||||
async-channel = "1.8.0"
|
||||
futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||
reqwest = { version = "0.11.12", features = ["json"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.3.6", features = ["async_tokio"] }
|
||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
|
||||
@@ -120,6 +120,15 @@ $ cargo test -- --ignored
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
|
||||
## Update Provider Data
|
||||
|
||||
To add the updates from the
|
||||
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
|
||||
|
||||
```
|
||||
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
|
||||
```
|
||||
|
||||
## Language bindings and frontend projects
|
||||
|
||||
Language bindings are available for:
|
||||
|
||||
@@ -38,11 +38,64 @@ Hello {i}",
|
||||
context
|
||||
}
|
||||
|
||||
/// Receive 100 emails that remove charlie@example.com and add
|
||||
/// him back
|
||||
async fn recv_groupmembership_emails(context: Context) -> Context {
|
||||
for i in 0..50 {
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||
From: sender@testrun.org
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
Chat-Group-Member-Added: charlie@example.com
|
||||
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||
From: sender@testrun.org
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
Chat-Group-Member-Removed: charlie@example.com
|
||||
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
let context = Context::new(dbfile.as_path(), id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -52,7 +105,7 @@ async fn create_context() -> Context {
|
||||
|
||||
if backup.exists() {
|
||||
println!("Importing backup");
|
||||
imex(&context, ImexMode::ImportBackup, &backup, None)
|
||||
imex(&context, ImexMode::ImportBackup, backup.as_path(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -83,6 +136,20 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
});
|
||||
});
|
||||
group.bench_function(
|
||||
"Receive 100 Chat-Group-Member-{Added|Removed} messages",
|
||||
|b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(create_context());
|
||||
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
async move {
|
||||
recv_groupmembership_emails(black_box(ctx)).await;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
group.finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1659,7 +1659,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image))
|
||||
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), &to_string_lossy(image))
|
||||
.await
|
||||
.map(|_| 1)
|
||||
.unwrap_or_log_default(ctx, "Failed to set profile image")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
@@ -19,21 +19,21 @@ num-traits = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.6.1" }
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.25" }
|
||||
serde_json = "1.0.87"
|
||||
serde_json = "1.0.89"
|
||||
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
|
||||
tokio = { version = "1.21.2" }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.22.0" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.2"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.5.17", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.9.1", optional = true }
|
||||
axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -733,7 +733,7 @@ impl CommandApi {
|
||||
image_path: Option<String>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default())
|
||||
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), &image_path.unwrap_or_default())
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ pub struct MessageObject {
|
||||
view_type: MessageViewtype,
|
||||
state: u32,
|
||||
|
||||
/// An error text, if there is one.
|
||||
error: Option<String>,
|
||||
|
||||
timestamp: i64,
|
||||
sort_timestamp: i64,
|
||||
received_timestamp: i64,
|
||||
@@ -167,6 +170,7 @@ impl MessageObject {
|
||||
.get_state()
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
||||
error: message.error(),
|
||||
|
||||
timestamp: message.get_timestamp(),
|
||||
sort_timestamp: message.get_sort_timestamp(),
|
||||
|
||||
1
deltachat-jsonrpc/typescript/.gitignore
vendored
1
deltachat-jsonrpc/typescript/.gitignore
vendored
@@ -6,3 +6,4 @@ yarn.lock
|
||||
package-lock.json
|
||||
docs
|
||||
accounts
|
||||
generated
|
||||
|
||||
@@ -1,948 +0,0 @@
|
||||
// AUTO-GENERATED by yerpc-derive
|
||||
|
||||
import * as T from "./types.js"
|
||||
import * as RPC from "./jsonrpc.js"
|
||||
|
||||
type RequestMethod = (method: string, params?: RPC.Params) => Promise<unknown>;
|
||||
type NotificationMethod = (method: string, params?: RPC.Params) => void;
|
||||
|
||||
interface Transport {
|
||||
request: RequestMethod,
|
||||
notification: NotificationMethod
|
||||
}
|
||||
|
||||
export class RawClient {
|
||||
constructor(private _transport: Transport) {}
|
||||
|
||||
/**
|
||||
* Check if an email address is valid.
|
||||
*/
|
||||
public checkEmailValidity(email: string): Promise<boolean> {
|
||||
return (this._transport.request('check_email_validity', [email] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get general system info.
|
||||
*/
|
||||
public getSystemInfo(): Promise<Record<string,string>> {
|
||||
return (this._transport.request('get_system_info', [] as RPC.Params)) as Promise<Record<string,string>>;
|
||||
}
|
||||
|
||||
|
||||
public addAccount(): Promise<T.U32> {
|
||||
return (this._transport.request('add_account', [] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public removeAccount(accountId: T.U32): Promise<null> {
|
||||
return (this._transport.request('remove_account', [accountId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getAllAccountIds(): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_all_account_ids', [] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select account id for internally selected state.
|
||||
* TODO: Likely this is deprecated as all methods take an account id now.
|
||||
*/
|
||||
public selectAccount(id: T.U32): Promise<null> {
|
||||
return (this._transport.request('select_account', [id] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected account id of the internal state..
|
||||
* TODO: Likely this is deprecated as all methods take an account id now.
|
||||
*/
|
||||
public getSelectedAccountId(): Promise<(T.U32|null)> {
|
||||
return (this._transport.request('get_selected_account_id', [] as RPC.Params)) as Promise<(T.U32|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all configured accounts.
|
||||
*/
|
||||
public getAllAccounts(): Promise<(T.Account)[]> {
|
||||
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
|
||||
}
|
||||
|
||||
|
||||
public startIoForAllAccounts(): Promise<null> {
|
||||
return (this._transport.request('start_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public stopIoForAllAccounts(): Promise<null> {
|
||||
return (this._transport.request('stop_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public startIo(id: T.U32): Promise<null> {
|
||||
return (this._transport.request('start_io', [id] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public stopIo(id: T.U32): Promise<null> {
|
||||
return (this._transport.request('stop_io', [id] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top-level info for an account.
|
||||
*/
|
||||
public getAccountInfo(accountId: T.U32): Promise<T.Account> {
|
||||
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined filesize of an account in bytes
|
||||
*/
|
||||
public getAccountFileSize(accountId: T.U32): Promise<T.U64> {
|
||||
return (this._transport.request('get_account_file_size', [accountId] as RPC.Params)) as Promise<T.U64>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getProviderInfo(accountId: T.U32, email: string): Promise<(T.ProviderInfo|null)> {
|
||||
return (this._transport.request('get_provider_info', [accountId, email] as RPC.Params)) as Promise<(T.ProviderInfo|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the context is already configured.
|
||||
*/
|
||||
public isConfigured(accountId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('is_configured', [accountId] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system info for an account.
|
||||
*/
|
||||
public getInfo(accountId: T.U32): Promise<Record<string,string>> {
|
||||
return (this._transport.request('get_info', [accountId] as RPC.Params)) as Promise<Record<string,string>>;
|
||||
}
|
||||
|
||||
|
||||
public setConfig(accountId: T.U32, key: string, value: (string|null)): Promise<null> {
|
||||
return (this._transport.request('set_config', [accountId, key, value] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public batchSetConfig(accountId: T.U32, config: Record<string,(string|null)>): Promise<null> {
|
||||
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
* Before this function is called, `checkQr()` should confirm the type of the
|
||||
* QR code is `account` or `webrtcInstance`.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
*/
|
||||
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
|
||||
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
|
||||
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
|
||||
}
|
||||
|
||||
|
||||
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
|
||||
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
|
||||
}
|
||||
|
||||
|
||||
public batchGetConfig(accountId: T.U32, keys: (string)[]): Promise<Record<string,(string|null)>> {
|
||||
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
|
||||
}
|
||||
|
||||
|
||||
public setStockStrings(strings: Record<T.U32,string>): Promise<null> {
|
||||
return (this._transport.request('set_stock_strings', [strings] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this account with the currently set parameters.
|
||||
* Setup the credential config before calling this.
|
||||
*/
|
||||
public configure(accountId: T.U32): Promise<null> {
|
||||
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal an ongoing process to stop.
|
||||
*/
|
||||
public stopOngoingProcess(accountId: T.U32): Promise<null> {
|
||||
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public exportSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('export_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public importSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('import_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all _fresh_ messages of any chat.
|
||||
* Typically used for implementing notification summaries
|
||||
* or badge counters e.g. on the app icon.
|
||||
* The list is already sorted and starts with the most recent fresh message.
|
||||
*
|
||||
* Messages belonging to muted chats or to the contact requests are not returned;
|
||||
* these messages should not be notified
|
||||
* and also badge counters should not include these messages.
|
||||
*
|
||||
* To get the number of fresh messages for a single chat, muted or not,
|
||||
* use `get_fresh_msg_cnt()`.
|
||||
*/
|
||||
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of _fresh_ messages in a chat.
|
||||
* Typically used to implement a badge with a number in the chatlist.
|
||||
*
|
||||
* If the specified chat is muted,
|
||||
* the UI should show the badge counter "less obtrusive",
|
||||
* e.g. using "gray" instead of "red" color.
|
||||
*/
|
||||
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
|
||||
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user
|
||||
* before actually enabling deletion of old messages.
|
||||
*/
|
||||
public estimateAutoDeletionCount(accountId: T.U32, fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
|
||||
return (this._transport.request('estimate_auto_deletion_count', [accountId, fromServer, seconds] as RPC.Params)) as Promise<T.Usize>;
|
||||
}
|
||||
|
||||
|
||||
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('initiate_autocrypt_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
public continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
|
||||
return (this._transport.request('continue_autocrypt_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getChatlistEntries(accountId: T.U32, listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
|
||||
return (this._transport.request('get_chatlist_entries', [accountId, listFlags, queryString, queryContactId] as RPC.Params)) as Promise<(T.ChatListEntry)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getChatlistItemsByEntries(accountId: T.U32, entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
|
||||
return (this._transport.request('get_chatlist_items_by_entries', [accountId, entries] as RPC.Params)) as Promise<Record<T.U32,T.ChatListItemFetchResult>>;
|
||||
}
|
||||
|
||||
|
||||
public getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
|
||||
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
|
||||
}
|
||||
|
||||
/**
|
||||
* get basic info about a chat,
|
||||
* use chatlist_get_full_chat_by_id() instead if you need more information
|
||||
*/
|
||||
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
|
||||
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
|
||||
}
|
||||
|
||||
|
||||
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public blockChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat.
|
||||
*
|
||||
* Messages are deleted from the device and the chat database entry is deleted.
|
||||
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
|
||||
*
|
||||
* Things that are _not done_ implicitly:
|
||||
*
|
||||
* - Messages are **not deleted from the server**.
|
||||
* - 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 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,
|
||||
* (3) only leaving groups is also a valid usecase.
|
||||
*
|
||||
* To leave a chat explicitly, use leave_group()
|
||||
*/
|
||||
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a chat.
|
||||
* Get a multi-line encryption info, containing encryption preferences of all members.
|
||||
* Can be used to find out why messages sent to group are not encrypted.
|
||||
*
|
||||
* returns Multi-line text
|
||||
*/
|
||||
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* The QR code is compatible to the OPENPGP4FPR format
|
||||
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
*
|
||||
* The scanning device will pass the scanned content to `checkQr()` then;
|
||||
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
* an out-of-band-verification can be joined using `secure_join()`
|
||||
*
|
||||
* chat_id: If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* return format: `[code, svg]`
|
||||
*/
|
||||
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
|
||||
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
* This function is typically called when `check_qr()` returns
|
||||
* type=AskVerifyContact or type=AskVerifyGroup.
|
||||
*
|
||||
* The function returns immediately and the handshake runs in background,
|
||||
* sending and receiving several messages.
|
||||
* During the handshake, info messages are added to the chat,
|
||||
* showing progress, success or errors.
|
||||
*
|
||||
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
* to `check_qr()`.
|
||||
*
|
||||
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
|
||||
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
*
|
||||
*/
|
||||
public secureJoin(accountId: T.U32, qr: string): Promise<T.U32> {
|
||||
return (this._transport.request('secure_join', [accountId, qr] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from a group.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a group.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contact IDs belonging to a chat.
|
||||
*
|
||||
* - 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
|
||||
* explicitly as it may happen that oneself gets removed from a still existing
|
||||
* group
|
||||
*
|
||||
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||
*
|
||||
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
||||
* for now, the UI should not show the list for mailing lists.
|
||||
* (we do not know all members and there is not always a global mailing list address,
|
||||
* so we could return only SELF or the known members; this is not decided yet)
|
||||
*/
|
||||
public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group chat.
|
||||
*
|
||||
* After creation,
|
||||
* the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
* and is in _unpromoted_ state.
|
||||
* This means, you can add or remove members, change the name,
|
||||
* the group image and so on without messages being sent to all group members.
|
||||
*
|
||||
* This changes as soon as the first message is sent to the group members
|
||||
* and the group becomes _promoted_.
|
||||
* After that, all changes are synced with all group members
|
||||
* by sending status message.
|
||||
*
|
||||
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
|
||||
* This may be useful if you want to show some help for just created groups.
|
||||
*
|
||||
* @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
* Only verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
*/
|
||||
public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise<T.U32> {
|
||||
return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new broadcast list.
|
||||
*
|
||||
* Broadcast lists are similar to groups on the sending device,
|
||||
* however, recipients get the messages in normal one-to-one chats
|
||||
* and will not be aware of other members.
|
||||
*
|
||||
* Replies to broadcasts go only to the sender
|
||||
* and not to all broadcast recipients.
|
||||
* Moreover, replies will not appear in the broadcast list
|
||||
* but in the one-to-one chat with the person answering.
|
||||
*
|
||||
* The name and the image of the broadcast list is set automatically
|
||||
* and is visible to the sender only.
|
||||
* Not asking for these data allows more focused creation
|
||||
* and we bypass the question who will get which data.
|
||||
* Also, many users will have at most one broadcast list
|
||||
* so, a generic name and image is sufficient at the first place.
|
||||
*
|
||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
* All in all, this is also what other messengers are doing here.
|
||||
*/
|
||||
public createBroadcastList(accountId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group name.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise<null> {
|
||||
return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* To find out the profile image of a chat, use dc_chat_get_profile_image()
|
||||
*
|
||||
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
|
||||
* this change anyway).
|
||||
*/
|
||||
public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise<null> {
|
||||
return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public setChatVisibility(accountId: T.U32, chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
|
||||
return (this._transport.request('set_chat_visibility', [accountId, chatId, visibility] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public setChatEphemeralTimer(accountId: T.U32, chatId: T.U32, timer: T.U32): Promise<null> {
|
||||
return (this._transport.request('set_chat_ephemeral_timer', [accountId, chatId, timer] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getChatEphemeralTimer(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('get_chat_ephemeral_timer', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
|
||||
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
* but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
* (IMAP/MDNs is not done for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also markseen_msgs().
|
||||
*/
|
||||
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
|
||||
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mute duration of a chat.
|
||||
*
|
||||
* The UI can then call is_chat_muted() when receiving a new message
|
||||
* to decide whether it should trigger an notification.
|
||||
*
|
||||
* Muted chats should not sound or vibrate
|
||||
* and should not show a visual notification in the system area.
|
||||
* Moreover, muted chats should be excluded from global badge counter
|
||||
* (get_fresh_msgs() skips muted chats therefore)
|
||||
* and the in-app, per-chat badge counter should use a less obtrusive color.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED.
|
||||
*/
|
||||
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
|
||||
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
|
||||
*
|
||||
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
|
||||
*/
|
||||
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark messages as presented to the user.
|
||||
* Typically, UIs call this function on scrolling through the message list,
|
||||
* when the messages are presented at least for a little moment.
|
||||
* 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 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.
|
||||
*
|
||||
* - For contact requests, no IMAP or MDNs is done
|
||||
* and the internal state is not changed therefore.
|
||||
* See also marknoticed_chat().
|
||||
*
|
||||
* Moreover, timer is started for incoming ephemeral messages.
|
||||
* This also happens for contact requests chats.
|
||||
*
|
||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
*/
|
||||
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
|
||||
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageListItems(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
|
||||
return (this._transport.request('get_message_list_items', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.MessageListItem)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
|
||||
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
|
||||
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
|
||||
}
|
||||
|
||||
|
||||
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
||||
return (this._transport.request('get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch info desktop needs for creating a notification for a message
|
||||
*/
|
||||
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
|
||||
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* on the IMAP server.
|
||||
*/
|
||||
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
|
||||
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an informational text for a single message. The text is multiline and may
|
||||
* contain e.g. the raw text of the message.
|
||||
*
|
||||
* The max. text returned is typically longer (about 100000 characters) than the
|
||||
* max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||
*/
|
||||
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the core to start downloading a message fully.
|
||||
* This function is typically called when the user hits the "Download" button
|
||||
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
|
||||
*
|
||||
* On success, the @ref DC_MSG "view type of the message" may change
|
||||
* or the message may be replaced completely by one or more messages with other message IDs.
|
||||
* That may happen e.g. in cases where the message was encrypted
|
||||
* and the type could not be determined without fully downloading.
|
||||
* Downloaded content can be accessed as usual after download.
|
||||
*
|
||||
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
|
||||
*/
|
||||
public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise<null> {
|
||||
return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
|
||||
*
|
||||
* Global chat results are typically displayed using dc_msg_get_summary(), chat
|
||||
* search results may just hilite the corresponding messages and present a
|
||||
* prev/next button.
|
||||
*
|
||||
* For global search, result is limited to 1000 messages,
|
||||
* this allows incremental search done fast.
|
||||
* So, when getting exactly 1000 results, the result may be truncated;
|
||||
* the UIs may display sth. as "1000+ messages found" in this case.
|
||||
* Chat search (if a chat_id is set) is not limited.
|
||||
*/
|
||||
public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
|
||||
return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.MessageSearchResult>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single contact options by ID.
|
||||
*/
|
||||
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
|
||||
return (this._transport.request('get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single contact as a result of an explicit user action.
|
||||
*
|
||||
* Returns contact id of the created or existing contact
|
||||
*/
|
||||
public createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
|
||||
return (this._transport.request('create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns contact id of the created or existing DM chat with that contact
|
||||
*/
|
||||
public createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('block_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('unblock_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('get_blocked_contacts', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of contacts.
|
||||
* (formerly called getContacts2 in desktop)
|
||||
*/
|
||||
public getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
|
||||
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
|
||||
}
|
||||
|
||||
|
||||
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
public changeContactName(accountId: T.U32, contactId: T.U32, name: string): Promise<null> {
|
||||
return (this._transport.request('change_contact_name', [accountId, contactId, name] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a contact.
|
||||
* Get a multi-line encryption info, containing your fingerprint and the
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
*/
|
||||
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
||||
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
*
|
||||
* To validate an e-mail address independently of the contact database
|
||||
* use check_email_validity().
|
||||
*/
|
||||
public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> {
|
||||
return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a chat.
|
||||
* Typically used to show a gallery.
|
||||
*
|
||||
* The list is already sorted and starts with the oldest message.
|
||||
* Clients should not try to re-sort the list as this would be an expensive action
|
||||
* and would result in inconsistencies between clients.
|
||||
*
|
||||
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
|
||||
* from any chat of the currently used account.
|
||||
*/
|
||||
public getChatMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* one combined call for getting chat::get_next_media for both directions
|
||||
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
*/
|
||||
public getNeighboringChatMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
|
||||
return (this._transport.request('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
|
||||
}
|
||||
|
||||
|
||||
public exportBackup(accountId: T.U32, destination: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('export_backup', [accountId, destination, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public importBackup(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('import_backup', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the network likely has come back.
|
||||
* or just that the network conditions might have changed
|
||||
*/
|
||||
public maybeNetwork(): Promise<null> {
|
||||
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||
* One of:
|
||||
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
|
||||
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
|
||||
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
|
||||
*
|
||||
* We don't use exact values but ranges here so that we can split up
|
||||
* states into multiple states in the future.
|
||||
*
|
||||
* Meant as a rough overview that can be shown
|
||||
* e.g. in the title of the main screen.
|
||||
*
|
||||
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
|
||||
*/
|
||||
public getConnectivity(accountId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an overview of the current connectivity, and possibly more statistics.
|
||||
* Meant to give the user more insight about the current status than
|
||||
* the basic connectivity info returned by get_connectivity(); show this
|
||||
* e.g., if the user taps on said basic connectivity info.
|
||||
*
|
||||
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
|
||||
*
|
||||
* This comes as an HTML from the core so that we can easily improve it
|
||||
* and the improvement instantly reaches all UIs.
|
||||
*/
|
||||
public getConnectivityHtml(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
public getLocations(accountId: T.U32, chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
|
||||
return (this._transport.request('get_locations', [accountId, chatId, contactId, timestampBegin, timestampEnd] as RPC.Params)) as Promise<(T.Location)[]>;
|
||||
}
|
||||
|
||||
|
||||
public sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
|
||||
return (this._transport.request('send_webxdc_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_webxdc_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info from a webxdc message
|
||||
*/
|
||||
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
|
||||
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward messages to another chat.
|
||||
*
|
||||
* All types of messages can be forwarded,
|
||||
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
|
||||
*
|
||||
* Original sender, info-state and webxdc updates are not forwarded on purpose.
|
||||
*/
|
||||
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public sendSticker(accountId: T.U32, chatId: T.U32, stickerPath: string): Promise<T.U32> {
|
||||
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*/
|
||||
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
|
||||
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft for a chat, if any.
|
||||
*/
|
||||
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
|
||||
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
|
||||
}
|
||||
|
||||
|
||||
public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* save a sticker to a collection/folder in the account's sticker folder
|
||||
*/
|
||||
public miscSaveSticker(accountId: T.U32, msgId: T.U32, collection: string): Promise<null> {
|
||||
return (this._transport.request('misc_save_sticker', [accountId, msgId, collection] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* for desktop, get stickers from stickers folder,
|
||||
* grouped by the collection/folder they are in.
|
||||
*/
|
||||
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
|
||||
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messageid of the sent message
|
||||
*/
|
||||
public miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
|
||||
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
|
||||
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
|
||||
}
|
||||
|
||||
|
||||
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
|
||||
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK = 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL = 9,
|
||||
DC_CHAT_ID_TRASH = 3,
|
||||
DC_CHAT_TYPE_BROADCAST = 160,
|
||||
DC_CHAT_TYPE_GROUP = 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST = 140,
|
||||
DC_CHAT_TYPE_SINGLE = 100,
|
||||
DC_CHAT_TYPE_UNDEFINED = 0,
|
||||
DC_CONNECTIVITY_CONNECTED = 4000,
|
||||
DC_CONNECTIVITY_CONNECTING = 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
|
||||
DC_CONNECTIVITY_WORKING = 3000,
|
||||
DC_CONTACT_ID_DEVICE = 5,
|
||||
DC_CONTACT_ID_INFO = 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9,
|
||||
DC_CONTACT_ID_SELF = 1,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
DC_GCL_ARCHIVED_ONLY = 1,
|
||||
DC_GCL_FOR_FORWARDING = 8,
|
||||
DC_GCL_NO_SPECIALS = 2,
|
||||
DC_GCL_VERIFIED_ONLY = 1,
|
||||
DC_GCM_ADDDAYMARKER = 1,
|
||||
DC_GCM_INFO_ONLY = 2,
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
DC_MEDIA_QUALITY_WORSE = 1,
|
||||
DC_MSG_ID_DAYMARKER = 9,
|
||||
DC_MSG_ID_LAST_SPECIAL = 9,
|
||||
DC_MSG_ID_MARKER1 = 1,
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
|
||||
DC_SHOW_EMAILS_ALL = 2,
|
||||
DC_SHOW_EMAILS_OFF = 0,
|
||||
DC_SOCKET_AUTO = 0,
|
||||
DC_SOCKET_PLAIN = 3,
|
||||
DC_SOCKET_SSL = 1,
|
||||
DC_SOCKET_STARTTLS = 2,
|
||||
DC_STATE_IN_FRESH = 10,
|
||||
DC_STATE_IN_NOTICED = 13,
|
||||
DC_STATE_IN_SEEN = 16,
|
||||
DC_STATE_OUT_DELIVERED = 26,
|
||||
DC_STATE_OUT_DRAFT = 19,
|
||||
DC_STATE_OUT_FAILED = 24,
|
||||
DC_STATE_OUT_MDN_RCVD = 28,
|
||||
DC_STATE_OUT_PENDING = 20,
|
||||
DC_STATE_OUT_PREPARING = 18,
|
||||
DC_STATE_UNDEFINED = 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||
DC_STR_ADD_MEMBER_BY_OTHER = 129,
|
||||
DC_STR_ADD_MEMBER_BY_YOU = 128,
|
||||
DC_STR_AEAP_ADDR_CHANGED = 122,
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||
DC_STR_CONTACT_VERIFIED = 35,
|
||||
DC_STR_DEVICE_MESSAGES = 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT = 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY = 100,
|
||||
DC_STR_DRAFT = 3,
|
||||
DC_STR_E2E_AVAILABLE = 25,
|
||||
DC_STR_E2E_PREFERRED = 34,
|
||||
DC_STR_ENCRYPTEDMSG = 24,
|
||||
DC_STR_ENCR_NONE = 28,
|
||||
DC_STR_ENCR_TRANSP = 27,
|
||||
DC_STR_EPHEMERAL_DAY = 79,
|
||||
DC_STR_EPHEMERAL_DAYS = 95,
|
||||
DC_STR_EPHEMERAL_DISABLED = 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
|
||||
DC_STR_EPHEMERAL_HOUR = 78,
|
||||
DC_STR_EPHEMERAL_HOURS = 94,
|
||||
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
|
||||
DC_STR_EPHEMERAL_WEEK = 80,
|
||||
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||
DC_STR_ERROR = 112,
|
||||
DC_STR_ERROR_NO_NETWORK = 87,
|
||||
DC_STR_FAILED_SENDING_TO = 74,
|
||||
DC_STR_FILE = 12,
|
||||
DC_STR_FINGERPRINTS = 30,
|
||||
DC_STR_FORWARDED = 97,
|
||||
DC_STR_GIF = 23,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
|
||||
DC_STR_GROUP_LEFT_BY_OTHER = 133,
|
||||
DC_STR_GROUP_LEFT_BY_YOU = 132,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
DC_STR_MSGDELMEMBER = 18,
|
||||
DC_STR_MSGGROUPLEFT = 19,
|
||||
DC_STR_MSGGRPIMGCHANGED = 16,
|
||||
DC_STR_MSGGRPIMGDELETED = 33,
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
DC_STR_ONE_MOMENT = 106,
|
||||
DC_STR_OUTGOING_MESSAGES = 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
DC_STR_SAVED_MESSAGES = 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES = 118,
|
||||
DC_STR_SECURE_JOIN_STARTED = 117,
|
||||
DC_STR_SELF = 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY = 91,
|
||||
DC_STR_SENDING = 110,
|
||||
DC_STR_SERVER_TURNED_OFF = 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC = 119,
|
||||
DC_STR_STICKER = 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN = 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
|
||||
DC_STR_SYNC_MSG_BODY = 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT = 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
|
||||
DC_STR_UPDATING = 109,
|
||||
DC_STR_VIDEO = 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION = 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
|
||||
DC_VIDEOCHATTYPE_JITSI = 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN = 0,
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type U32=number;
|
||||
export type Usize=number;
|
||||
export type Event=(({
|
||||
/**
|
||||
* The library-user may write an informational string to the log.
|
||||
*
|
||||
* This event should *not* be reported to the end-user using a popup or something like
|
||||
* that.
|
||||
*/
|
||||
"type":"Info";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when SMTP connection is established and login was successful.
|
||||
*/
|
||||
"type":"SmtpConnected";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when IMAP connection is established and login was successful.
|
||||
*/
|
||||
"type":"ImapConnected";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when a message was successfully sent to the SMTP server.
|
||||
*/
|
||||
"type":"SmtpMessageSent";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an IMAP message has been marked as deleted
|
||||
*/
|
||||
"type":"ImapMessageDeleted";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an IMAP message has been moved
|
||||
*/
|
||||
"type":"ImapMessageMoved";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an new file in the $BLOBDIR was created
|
||||
*/
|
||||
"type":"NewBlobFile";}&{"file":string;})|({
|
||||
/**
|
||||
* Emitted when an file in the $BLOBDIR was deleted
|
||||
*/
|
||||
"type":"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.
|
||||
*/
|
||||
"type":"Warning";}&{"msg":string;})|({
|
||||
/**
|
||||
* The library-user should report an error to the end-user.
|
||||
*
|
||||
* As most things are asynchronous, things may go wrong at any time and the user
|
||||
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
*
|
||||
* However, for ongoing processes (eg. configure())
|
||||
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||
* 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 messasge box then.
|
||||
*/
|
||||
"type":"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.
|
||||
*/
|
||||
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
|
||||
/**
|
||||
* Messages or chats changed. One or more messages or chats changed for various
|
||||
* reasons in the database:
|
||||
* - Messages sent, received or removed
|
||||
* - Chats created, deleted or archived
|
||||
* - A draft has been set
|
||||
*
|
||||
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||
*/
|
||||
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Reactions for the message changed.
|
||||
*/
|
||||
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
*
|
||||
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
*/
|
||||
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Downloading a bunch of messages just finished. This is an experimental
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
*
|
||||
* msg_ids contains the message ids.
|
||||
*/
|
||||
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
|
||||
/**
|
||||
* Messages were seen or noticed.
|
||||
* chat id is always set.
|
||||
*/
|
||||
"type":"MsgsNoticed";}&{"chatId":U32;})|({
|
||||
/**
|
||||
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
* DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
* DC_STATE_OUT_FAILED, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
* See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
* and removeContactFromChat().
|
||||
*
|
||||
* This event does not include ephemeral timer modification, which
|
||||
* is a separate event.
|
||||
*/
|
||||
"type":"ChatModified";}&{"chatId":U32;})|({
|
||||
/**
|
||||
* Chat ephemeral timer changed.
|
||||
*/
|
||||
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
*/
|
||||
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
|
||||
/**
|
||||
* Location of one or more contact has changed.
|
||||
*
|
||||
* @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
* If the locations of several contacts have been changed,
|
||||
* this parameter is set to `None`.
|
||||
*/
|
||||
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
|
||||
/**
|
||||
* Inform about the configuration progress started by configure().
|
||||
*/
|
||||
"type":"ConfigureProgress";}&{
|
||||
/**
|
||||
* Progress.
|
||||
*
|
||||
* 0=error, 1-999=progress in permille, 1000=success and done
|
||||
*/
|
||||
"progress":Usize;
|
||||
/**
|
||||
* Progress comment or error, something to display to the user.
|
||||
*/
|
||||
"comment":(string|null);})|({
|
||||
/**
|
||||
* Inform about the import/export progress started by imex().
|
||||
*
|
||||
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
* @param data2 0
|
||||
*/
|
||||
"type":"ImexProgress";}&{"progress":Usize;})|({
|
||||
/**
|
||||
* A file has been exported. A file has been written by imex().
|
||||
* This event may be sent multiple times by a single call to imex().
|
||||
*
|
||||
* A typical purpose for a handler of this event may be to make the file public to some system
|
||||
* services.
|
||||
*
|
||||
* @param data2 0
|
||||
*/
|
||||
"type":"ImexFileWritten";}&{"path":string;})|({
|
||||
/**
|
||||
* Progress information of a secure-join handshake from the view of the inviter
|
||||
* (Alice, the person who shows the QR code).
|
||||
*
|
||||
* These events are typically sent after a joiner has scanned the QR code
|
||||
* generated by getChatSecurejoinQrCodeSvg().
|
||||
*
|
||||
* @param data1 (int) ID of the contact that wants to join.
|
||||
* @param data2 (int) Progress as:
|
||||
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
*/
|
||||
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
|
||||
/**
|
||||
* Progress information of a secure-join handshake from the view of the joiner
|
||||
* (Bob, the person who scans the QR code).
|
||||
* The events are typically sent while secureJoin(), which
|
||||
* may take some time, is executed.
|
||||
* @param data1 (int) ID of the inviting contact.
|
||||
* @param data2 (int) Progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
*/
|
||||
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
|
||||
/**
|
||||
* The connectivity to the server changed.
|
||||
* This means that you should refresh the connectivity view
|
||||
* and possibly the connectivtiy HTML; see getConnectivity() and
|
||||
* getConnectivityHtml() for details.
|
||||
*/
|
||||
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
|
||||
/**
|
||||
* Inform that a message containing a webxdc instance has been deleted
|
||||
*/
|
||||
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));
|
||||
@@ -1,10 +0,0 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;});
|
||||
export type Params=((JSONValue)[]|Record<string,JSONValue>);
|
||||
export type U32=number;
|
||||
export type Request={"jsonrpc":"2.0";"method":string;"params"?:Params;"id"?:U32;};
|
||||
export type I32=number;
|
||||
export type Error={"code":I32;"message":string;"data"?:JSONValue;};
|
||||
export type Response={"jsonrpc":"2.0";"id":(U32|null);"result"?:JSONValue;"error"?:Error;};
|
||||
export type Message=(Request|Response);
|
||||
@@ -1,198 +0,0 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type U64=number;
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type Usize=number;
|
||||
export type I64=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
|
||||
/**
|
||||
* true when chat is a broadcastlist
|
||||
*/
|
||||
"isBroadcast":boolean;
|
||||
/**
|
||||
* contact id if this is a dm chat (for view profile entry in context menu)
|
||||
*/
|
||||
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
|
||||
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
|
||||
/**
|
||||
* the contact's last seen timestamp
|
||||
*/
|
||||
"lastSeen":I64;"wasSeenRecently":boolean;};
|
||||
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":(string|null);};
|
||||
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
export type BasicChat=
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
|
||||
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
|
||||
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
|
||||
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
|
||||
/**
|
||||
* Day marker, separating messages that correspond to different
|
||||
* days according to local time.
|
||||
*/
|
||||
"kind":"dayMarker";}&{
|
||||
/**
|
||||
* Marker timestamp, for day markers, in unix milliseconds
|
||||
*/
|
||||
"timestamp":I64;}));
|
||||
export type Viewtype=("Unknown"|
|
||||
/**
|
||||
* Text message.
|
||||
*/
|
||||
"Text"|
|
||||
/**
|
||||
* Image message.
|
||||
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
|
||||
*/
|
||||
"Image"|
|
||||
/**
|
||||
* Animated GIF message.
|
||||
*/
|
||||
"Gif"|
|
||||
/**
|
||||
* Message containing a sticker, similar to image.
|
||||
* If possible, the ui should display the image without borders in a transparent way.
|
||||
* A click on a sticker will offer to install the sticker set in some future.
|
||||
*/
|
||||
"Sticker"|
|
||||
/**
|
||||
* Message containing an Audio file.
|
||||
*/
|
||||
"Audio"|
|
||||
/**
|
||||
* A voice message that was directly recorded by the user.
|
||||
* For all other audio messages, the type `Viewtype.Audio` should be used.
|
||||
*/
|
||||
"Voice"|
|
||||
/**
|
||||
* Video messages.
|
||||
*/
|
||||
"Video"|
|
||||
/**
|
||||
* Message containing any file, eg. a PDF.
|
||||
*/
|
||||
"File"|
|
||||
/**
|
||||
* Message is an invitation to a videochat.
|
||||
*/
|
||||
"VideochatInvitation"|
|
||||
/**
|
||||
* Message is an webxdc instance.
|
||||
*/
|
||||
"Webxdc");
|
||||
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
|
||||
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
|
||||
/**
|
||||
* Chat ephemeral message timer is changed.
|
||||
*/
|
||||
"EphemeralTimerChanged"|"ChatProtectionEnabled"|"ChatProtectionDisabled"|
|
||||
/**
|
||||
* Self-sent-message that contains only json used for multi-device-sync;
|
||||
* if possible, we attach that to other messages as for locations.
|
||||
*/
|
||||
"MultiDeviceSync"|"WebxdcStatusUpdate"|
|
||||
/**
|
||||
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||
*/
|
||||
"WebxdcInfoMessage");
|
||||
export type I32=number;
|
||||
export type WebxdcMessageInfo={
|
||||
/**
|
||||
* The name of the app.
|
||||
*
|
||||
* Defaults to the filename if not set in the manifest.
|
||||
*/
|
||||
"name":string;
|
||||
/**
|
||||
* App icon file name.
|
||||
* Defaults to an standard icon if nothing is set in the manifest.
|
||||
*
|
||||
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
|
||||
*
|
||||
* App icons should should be square,
|
||||
* the implementations will add round corners etc. as needed.
|
||||
*/
|
||||
"icon":string;
|
||||
/**
|
||||
* if the Webxdc represents a document, then this is the name of the document
|
||||
*/
|
||||
"document":(string|null);
|
||||
/**
|
||||
* short string describing the state of the app,
|
||||
* sth. as "2 votes", "Highscore: 123",
|
||||
* can be changed by the apps
|
||||
*/
|
||||
"summary":(string|null);
|
||||
/**
|
||||
* URL where the source code of the Webxdc and other information can be found;
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
*/
|
||||
"sourceCodeUrl":(string|null);
|
||||
/**
|
||||
* True if full internet access should be granted to the app.
|
||||
*/
|
||||
"internetAccess":boolean;};
|
||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||
|
||||
/**
|
||||
* Structure representing all reactions to a particular message.
|
||||
*/
|
||||
export type Reactions=
|
||||
/**
|
||||
* Structure representing all reactions to a particular message.
|
||||
*/
|
||||
{
|
||||
/**
|
||||
* Map from a contact to it's reaction to message.
|
||||
*/
|
||||
"reactionsByContact":Record<U32,(string)[]>;
|
||||
/**
|
||||
* Unique reactions and their count
|
||||
*/
|
||||
"reactions":Record<string,U32>;};
|
||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;
|
||||
/**
|
||||
* when is_info is true this describes what type of system message it is
|
||||
*/
|
||||
"systemMessageType":SystemMessageType;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
|
||||
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
|
||||
/**
|
||||
* also known as summary_text1
|
||||
*/
|
||||
"summaryPrefix":(string|null);
|
||||
/**
|
||||
* also known as summary_text2
|
||||
*/
|
||||
"summaryText":string;};
|
||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||
export type F64=number;
|
||||
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||
@@ -48,5 +48,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.101.0"
|
||||
"version": "1.103.0"
|
||||
}
|
||||
41
deltachat-rpc-client/README.md
Normal file
41
deltachat-rpc-client/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Delta Chat RPC python client
|
||||
|
||||
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
||||
and provides asynchronous interface to it.
|
||||
|
||||
## Getting started
|
||||
|
||||
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
Install it anywhere in your `PATH`.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Run `PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
## Using in REPL
|
||||
|
||||
Setup a development environment:
|
||||
```
|
||||
$ tox --devenv env
|
||||
$ . env/bin/activate
|
||||
```
|
||||
|
||||
It is recommended to use IPython, because it supports using `await` directly
|
||||
from the REPL.
|
||||
|
||||
```
|
||||
$ pip install ipython
|
||||
$ PATH="../target/debug:$PATH" ipython
|
||||
...
|
||||
In [1]: from deltachat_rpc_client import *
|
||||
In [2]: rpc = Rpc()
|
||||
In [3]: await rpc.start()
|
||||
In [4]: dc = DeltaChat(rpc)
|
||||
In [5]: system_info = await dc.get_system_info()
|
||||
In [6]: system_info["level"]
|
||||
Out[6]: 'awesome'
|
||||
In [7]: await rpc.close()
|
||||
```
|
||||
54
deltachat-rpc-client/examples/echobot.py
Executable file
54
deltachat-rpc-client/examples/echobot.py
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import deltachat_rpc_client as dc
|
||||
|
||||
|
||||
async def main():
|
||||
async with dc.Rpc() as rpc:
|
||||
deltachat = dc.DeltaChat(rpc)
|
||||
system_info = await deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
|
||||
await account.set_config("bot", "1")
|
||||
if not await account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
await account.set_config("addr", sys.argv[1])
|
||||
await account.set_config("mail_pw", sys.argv[2])
|
||||
await account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
await deltachat.start_io()
|
||||
|
||||
async def process_messages():
|
||||
for message in await account.get_fresh_messages_in_arrival_order():
|
||||
snapshot = await message.get_snapshot()
|
||||
if not snapshot.is_info:
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
await process_messages()
|
||||
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
if event["type"] == "Info":
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["type"] == "Warning":
|
||||
logging.warning("%s", event["msg"])
|
||||
elif event["type"] == "Error":
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["type"] == "IncomingMsg":
|
||||
logging.info("Got an incoming message")
|
||||
await process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
29
deltachat-rpc-client/pyproject.toml
Normal file
29
deltachat-rpc-client/pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"aiodns"
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
# We declare the package not-zip-safe so that our type hints are also available
|
||||
# when checking client code that uses our (installed) package.
|
||||
# Ref:
|
||||
# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561
|
||||
zip-safe = false
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
"py.typed"
|
||||
]
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
@@ -0,0 +1,5 @@
|
||||
from .account import Account
|
||||
from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
79
deltachat-rpc-client/src/deltachat_rpc_client/account.py
Normal file
79
deltachat-rpc-client/src/deltachat_rpc_client/account.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from .chat import Chat
|
||||
from .contact import Contact
|
||||
from .message import Message
|
||||
|
||||
|
||||
class Account:
|
||||
def __init__(self, rpc, account_id) -> None:
|
||||
self._rpc = rpc
|
||||
self.account_id = account_id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Account id={self.account_id}>"
|
||||
|
||||
async def wait_for_event(self) -> dict:
|
||||
"""Wait until the next event and return it."""
|
||||
return await self._rpc.wait_for_event(self.account_id)
|
||||
|
||||
async def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
await self._rpc.remove_account(self.account_id)
|
||||
|
||||
async def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
await self._rpc.start_io(self.account_id)
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
"""Stop the account I/O."""
|
||||
await self._rpc.stop_io(self.account_id)
|
||||
|
||||
async def get_info(self) -> dict:
|
||||
return await self._rpc.get_info(self.account_id)
|
||||
|
||||
async def get_file_size(self) -> int:
|
||||
return await self._rpc.get_account_file_size(self.account_id)
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
"""Return True for configured accounts."""
|
||||
return await self._rpc.is_configured(self.account_id)
|
||||
|
||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
"""Set the configuration value key pair."""
|
||||
await self._rpc.set_config(self.account_id, key, value)
|
||||
|
||||
async def get_config(self, key: str) -> Optional[str]:
|
||||
"""Get the configuration value."""
|
||||
return await self._rpc.get_config(self.account_id, key)
|
||||
|
||||
async def configure(self) -> None:
|
||||
"""Configure an account."""
|
||||
await self._rpc.configure(self.account_id)
|
||||
|
||||
async def create_contact(self, address: str, name: Optional[str] = None) -> Contact:
|
||||
"""Create a contact with the given address and, optionally, a name."""
|
||||
return Contact(
|
||||
self._rpc,
|
||||
self.account_id,
|
||||
await self._rpc.create_contact(self.account_id, address, name),
|
||||
)
|
||||
|
||||
async def secure_join(self, qrdata: str) -> Chat:
|
||||
chat_id = await self._rpc.secure_join(self.account_id, qrdata)
|
||||
return Chat(self._rpc, self.account_id, chat_id)
|
||||
|
||||
async def get_fresh_messages(self) -> List[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, use get_fresh_messages_in_arrival_order instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.account_id)
|
||||
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.account_id))
|
||||
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
|
||||
41
deltachat-rpc-client/src/deltachat_rpc_client/chat.py
Normal file
41
deltachat-rpc-client/src/deltachat_rpc_client/chat.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .rpc import Rpc
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message import Message
|
||||
|
||||
|
||||
class Chat:
|
||||
def __init__(self, rpc: Rpc, account_id: int, chat_id: int) -> None:
|
||||
self._rpc = rpc
|
||||
self.account_id = account_id
|
||||
self.chat_id = chat_id
|
||||
|
||||
async def block(self) -> None:
|
||||
"""Block the chat."""
|
||||
await self._rpc.block_chat(self.account_id, self.chat_id)
|
||||
|
||||
async def accept(self) -> None:
|
||||
"""Accept the contact request."""
|
||||
await self._rpc.accept_chat(self.account_id, self.chat_id)
|
||||
|
||||
async def delete(self) -> None:
|
||||
await self._rpc.delete_chat(self.account_id, self.chat_id)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
return await self._rpc.get_chat_encryption_info(self.account_id, self.chat_id)
|
||||
|
||||
async def send_text(self, text: str) -> "Message":
|
||||
from .message import Message
|
||||
|
||||
msg_id = await self._rpc.misc_send_text_message(
|
||||
self.account_id, self.chat_id, text
|
||||
)
|
||||
return Message(self._rpc, self.account_id, msg_id)
|
||||
|
||||
async def leave(self) -> None:
|
||||
await self._rpc.leave_group(self.account_id, self.chat_id)
|
||||
|
||||
async def get_fresh_message_count(self) -> int:
|
||||
return await self._rpc.get_fresh_msg_cnt(self.account_id, self.chat_id)
|
||||
52
deltachat-rpc-client/src/deltachat_rpc_client/contact.py
Normal file
52
deltachat-rpc-client/src/deltachat_rpc_client/contact.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .rpc import Rpc
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .chat import Chat
|
||||
|
||||
|
||||
class Contact:
|
||||
"""
|
||||
Contact API.
|
||||
|
||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||
"""
|
||||
|
||||
def __init__(self, rpc: Rpc, account_id: int, contact_id: int) -> None:
|
||||
self._rpc = rpc
|
||||
self.account_id = account_id
|
||||
self.contact_id = contact_id
|
||||
|
||||
async def block(self) -> None:
|
||||
"""Block contact."""
|
||||
await self._rpc.block_contact(self.account_id, self.contact_id)
|
||||
|
||||
async def unblock(self) -> None:
|
||||
"""Unblock contact."""
|
||||
await self._rpc.unblock_contact(self.account_id, self.contact_id)
|
||||
|
||||
async def delete(self) -> None:
|
||||
"""Delete contact."""
|
||||
await self._rpc.delete_contact(self.account_id, self.contact_id)
|
||||
|
||||
async def change_name(self, name: str) -> None:
|
||||
await self._rpc.change_contact_name(self.account_id, self.contact_id, name)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
return await self._rpc.get_contact_encryption_info(
|
||||
self.account_id, self.contact_id
|
||||
)
|
||||
|
||||
async def get_dictionary(self) -> dict:
|
||||
"""Return a dictionary with a snapshot of all contact properties."""
|
||||
return await self._rpc.get_contact(self.account_id, self.contact_id)
|
||||
|
||||
async def create_chat(self) -> "Chat":
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
self._rpc,
|
||||
self.account_id,
|
||||
await self._rpc.create_chat_by_contact_id(self.account_id, self.contact_id),
|
||||
)
|
||||
34
deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
Normal file
34
deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from typing import List
|
||||
|
||||
from .account import Account
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
class DeltaChat:
|
||||
"""
|
||||
Delta Chat account manager.
|
||||
This is the root of the object oriented API.
|
||||
"""
|
||||
|
||||
def __init__(self, rpc: Rpc) -> None:
|
||||
self.rpc = rpc
|
||||
|
||||
async def add_account(self) -> Account:
|
||||
account_id = await self.rpc.add_account()
|
||||
return Account(self.rpc, account_id)
|
||||
|
||||
async def get_all_accounts(self) -> List[Account]:
|
||||
account_ids = await self.rpc.get_all_account_ids()
|
||||
return [Account(self.rpc, account_id) for account_id in account_ids]
|
||||
|
||||
async def start_io(self) -> None:
|
||||
await self.rpc.start_io_for_all_accounts()
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
await self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
async def maybe_network(self) -> None:
|
||||
await self.rpc.maybe_network()
|
||||
|
||||
async def get_system_info(self) -> dict:
|
||||
return await self.rpc.get_system_info()
|
||||
42
deltachat-rpc-client/src/deltachat_rpc_client/message.py
Normal file
42
deltachat-rpc-client/src/deltachat_rpc_client/message.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from .chat import Chat
|
||||
from .contact import Contact
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, rpc: Rpc, account_id: int, msg_id: int) -> None:
|
||||
self._rpc = rpc
|
||||
self.account_id = account_id
|
||||
self.msg_id = msg_id
|
||||
|
||||
async def send_reaction(self, reactions: str) -> "Message":
|
||||
msg_id = await self._rpc.send_reaction(self.account_id, self.msg_id, reactions)
|
||||
return Message(self._rpc, self.account_id, msg_id)
|
||||
|
||||
async def get_snapshot(self) -> "MessageSnapshot":
|
||||
message_object = await self._rpc.get_message(self.account_id, self.msg_id)
|
||||
return MessageSnapshot(
|
||||
message=self,
|
||||
chat=Chat(self._rpc, self.account_id, message_object["chatId"]),
|
||||
sender=Contact(self._rpc, self.account_id, message_object["fromId"]),
|
||||
text=message_object["text"],
|
||||
error=message_object.get("error"),
|
||||
is_info=message_object["isInfo"],
|
||||
)
|
||||
|
||||
async def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account_id, [self.msg_id])
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageSnapshot:
|
||||
message: Message
|
||||
chat: Chat
|
||||
sender: Contact
|
||||
text: str
|
||||
error: Optional[str]
|
||||
is_info: bool
|
||||
1
deltachat-rpc-client/src/deltachat_rpc_client/py.typed
Normal file
1
deltachat-rpc-client/src/deltachat_rpc_client/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
# PEP 561 marker file. See https://peps.python.org/pep-0561/
|
||||
@@ -0,0 +1,52 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncGenerator, List
|
||||
|
||||
import aiohttp
|
||||
import pytest_asyncio
|
||||
|
||||
from .account import Account
|
||||
from .deltachat import DeltaChat
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
async def get_temp_credentials() -> dict:
|
||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url) as response:
|
||||
return json.loads(await response.text())
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
|
||||
async def new_configured_account(self) -> Account:
|
||||
credentials = await get_temp_credentials()
|
||||
account = await self.deltachat.add_account()
|
||||
assert not await account.is_configured()
|
||||
await account.set_config("addr", credentials["email"])
|
||||
await account.set_config("mail_pw", credentials["password"])
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
return account
|
||||
|
||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
||||
accounts = [await self.new_configured_account() for _ in range(num)]
|
||||
await self.deltachat.start_io()
|
||||
return accounts
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def rpc(tmp_path) -> AsyncGenerator:
|
||||
env = {**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")}
|
||||
rpc_server = Rpc(env=env)
|
||||
async with rpc_server as rpc:
|
||||
yield rpc
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def acfactory(rpc) -> AsyncGenerator:
|
||||
yield ACFactory(DeltaChat(rpc))
|
||||
92
deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
Normal file
92
deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, AsyncGenerator, Dict, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
async def start(self) -> None:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
*self._args,
|
||||
**self._kwargs
|
||||
)
|
||||
self.event_queues: Dict[int, asyncio.Queue] = {}
|
||||
self.id = 0
|
||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
||||
|
||||
# Map from request ID to `asyncio.Future` returning the response.
|
||||
self.request_events: Dict[int, asyncio.Future] = {}
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.process.terminate()
|
||||
await self.reader_task
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
while True:
|
||||
line = await self.process.stdout.readline()
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
elif response["method"] == "event":
|
||||
# An event notification.
|
||||
params = response["params"]
|
||||
account_id = params["contextId"]
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
await self.event_queues[account_id].put(params["event"])
|
||||
else:
|
||||
print(response)
|
||||
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
if account_id in self.event_queues:
|
||||
return await self.event_queues[account_id].get()
|
||||
return None
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
async def method(*args, **kwargs) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
assert not (args and kwargs), "Mixing positional and keyword arguments"
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": kwargs or args,
|
||||
"id": self.id,
|
||||
}
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
self.request_events[request_id] = fut
|
||||
response = await fut
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
|
||||
return method
|
||||
60
deltachat-rpc-client/tests/test_something.py
Normal file
60
deltachat-rpc-client/tests/test_something.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||
]
|
||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||
|
||||
for addr in valid_addresses:
|
||||
assert await rpc.check_email_validity(addr)
|
||||
for addr in invalid_addresses:
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
if event["type"] == "ConfigureProgress":
|
||||
# Progress 0 indicates error.
|
||||
assert event["progress"] != 0
|
||||
|
||||
if event["progress"] == 1000:
|
||||
# Success.
|
||||
break
|
||||
else:
|
||||
print(event)
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event["type"] == "IncomingMsg":
|
||||
chat_id = event["chatId"]
|
||||
msg_id = event["msgId"]
|
||||
break
|
||||
|
||||
rpc = acfactory.deltachat.rpc
|
||||
message = await rpc.get_message(bob.account_id, msg_id)
|
||||
assert message["chatId"] == chat_id
|
||||
assert message["text"] == "Hello!"
|
||||
19
deltachat-rpc-client/tox.ini
Normal file
19
deltachat-rpc-client/tox.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest {posargs}
|
||||
setenv =
|
||||
# Avoid stack overflow when Rust core is built without optimizations.
|
||||
RUST_MIN_STACK=8388608
|
||||
passenv =
|
||||
DCC_NEW_TMP_EMAIL
|
||||
deps =
|
||||
pytest
|
||||
pytest-async
|
||||
pytest-asyncio
|
||||
aiohttp
|
||||
aiodns
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.101.0"
|
||||
version = "1.103.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
@@ -17,10 +17,10 @@ name = "deltachat-rpc-server"
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.9.1" }
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.85"
|
||||
serde_json = "1.0.89"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.21.2", features = ["io-std"] }
|
||||
tokio = { version = "1.22.0", features = ["io-std"] }
|
||||
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }
|
||||
|
||||
@@ -4,7 +4,7 @@ This document gives a quick overview about the Webxdc specification,
|
||||
It is meant for both, developing Webxdc apps
|
||||
and developing Webxdc implementations.
|
||||
|
||||
The [Webxdc guidebook](https://deltachat.github.io/webxdc_docs/) shows more detailed information
|
||||
The [Webxdc guidebook](https://docs.webxdc.org/) shows more detailed information
|
||||
when developing Webxdc apps.
|
||||
|
||||
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.101.0"
|
||||
"version": "1.103.0"
|
||||
}
|
||||
@@ -156,6 +156,7 @@ def extract_defines(flags):
|
||||
| DC_KEY_GEN
|
||||
| DC_IMEX
|
||||
| DC_CONNECTIVITY
|
||||
| DC_DOWNLOAD
|
||||
) # End of prefix matching
|
||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||
) # Close the capturing group, this contains
|
||||
|
||||
@@ -82,12 +82,13 @@ class Account(object):
|
||||
if hasattr(db_path, "encode"):
|
||||
db_path = db_path.encode("utf8")
|
||||
|
||||
ptr = lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL)
|
||||
if ptr == ffi.NULL:
|
||||
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
|
||||
self._dc_context = ffi.gc(
|
||||
lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL),
|
||||
ptr,
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
if self._dc_context == ffi.NULL:
|
||||
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
|
||||
|
||||
self._shutdown_event = Event()
|
||||
self._event_thread = EventThread(self)
|
||||
|
||||
@@ -192,6 +192,12 @@ class FFIEventTracker:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_next_reactions_changed(self):
|
||||
"""wait for and return next reactions-changed message"""
|
||||
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
|
||||
assert ev.data1 > 0
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev.data1 == msg.chat.id
|
||||
@@ -296,6 +302,10 @@ class EventThread(threading.Thread):
|
||||
"ac_incoming_message",
|
||||
dict(message=msg),
|
||||
)
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_reactions_changed", dict(message=msg)
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", dict(message=msg)
|
||||
|
||||
@@ -49,6 +49,10 @@ class PerAccount:
|
||||
def ac_outgoing_message(self, message):
|
||||
"""Called on each outgoing message (both system and "normal")."""
|
||||
|
||||
@account_hookspec
|
||||
def ac_reactions_changed(self, message):
|
||||
"""Called when message reactions changed."""
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
"""Called when an outgoing message has been delivered to SMTP.
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Optional, Union
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
|
||||
from .reactions import Reactions
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -161,6 +162,17 @@ class Message(object):
|
||||
)
|
||||
)
|
||||
|
||||
def send_reaction(self, reaction: str):
|
||||
"""Send a reaction to message and return the resulting Message instance."""
|
||||
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
|
||||
if msg_id == 0:
|
||||
raise ValueError("reaction could not be send")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def get_reactions(self) -> Reactions:
|
||||
"""Get :class:`deltachat.reactions.Reactions` to the message."""
|
||||
return Reactions.from_msg(self)
|
||||
|
||||
def is_system_message(self):
|
||||
"""return True if this message is a system/info message."""
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
@@ -449,6 +461,17 @@ class Message(object):
|
||||
"""mark this message as seen."""
|
||||
self.account.mark_seen_messages([self.id])
|
||||
|
||||
#
|
||||
# Message download state
|
||||
#
|
||||
@property
|
||||
def download_state(self):
|
||||
assert self.id > 0
|
||||
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
|
||||
43
python/src/deltachat/reactions.py
Normal file
43
python/src/deltachat/reactions.py
Normal file
@@ -0,0 +1,43 @@
|
||||
""" The Reactions object. """
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_dc_charpointer, iter_array
|
||||
|
||||
|
||||
class Reactions(object):
|
||||
"""Reactions object.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.message.Message`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, dc_reactions):
|
||||
assert isinstance(account._dc_context, ffi.CData)
|
||||
assert isinstance(dc_reactions, ffi.CData)
|
||||
assert dc_reactions != ffi.NULL
|
||||
self.account = account
|
||||
self._dc_reactions = dc_reactions
|
||||
|
||||
def __repr__(self):
|
||||
return "<Reactions dc_reactions={}>".format(self._dc_reactions)
|
||||
|
||||
@classmethod
|
||||
def from_msg(cls, msg):
|
||||
assert msg.id > 0
|
||||
return cls(
|
||||
msg.account,
|
||||
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
|
||||
)
|
||||
|
||||
def get_contacts(self) -> list:
|
||||
"""Get list of contacts reacted to the message.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
|
||||
|
||||
def get_by_contact(self, contact) -> str:
|
||||
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
|
||||
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
@@ -405,6 +406,29 @@ def test_forward_own_message(acfactory, lp):
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
|
||||
def test_long_group_name(acfactory, lp):
|
||||
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
|
||||
group names after MIME serialization/deserialization".
|
||||
|
||||
When the mailadm bot creates a group with botadmin, the bot creates is as
|
||||
"pytest-supportuser-282@x.testrun.org support group" (for example). But in the botadmin's
|
||||
account object, the group chat is called " pytest-supportuser-282@x.testrun.org support group"
|
||||
(with an additional space character in the beginning).
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("ac1: creating group chat and sending a message")
|
||||
group_name = "pytest-supportuser-282@x.testrun.org support group"
|
||||
group = ac1.create_group_chat(group_name)
|
||||
group.add_contact(ac2)
|
||||
group.send_text("message")
|
||||
|
||||
# wait for other account to receive
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.chat.get_name() == group_name
|
||||
|
||||
|
||||
def test_send_self_message(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
@@ -901,6 +925,34 @@ def test_dont_show_emails(acfactory, lp):
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org, unkwnown.add@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message2@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: alice@example.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message3@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
@@ -926,7 +978,9 @@ def test_dont_show_emails(acfactory, lp):
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
assert msg.text == "subj – message in Sent"
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 2
|
||||
assert any(msg.text == "subj – Actually interesting message in Spam" for msg in chat_msgs)
|
||||
|
||||
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
|
||||
ac1.direct_imap.select_folder("Spam")
|
||||
@@ -942,7 +996,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory, lp):
|
||||
@@ -1237,6 +1291,103 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 32768
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
reactions_queue = queue.Queue()
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_reactions_changed(self, message):
|
||||
reactions_queue.put(message)
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmpdir.join("large")
|
||||
with open(path, "wb") as fout:
|
||||
fout.write(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(path.strpath))
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1_addr
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
lp.sec("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(2)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2.direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
|
||||
ac2.direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
lp.sec("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.text == msg1.text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1.get_config("addr")
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ deps =
|
||||
pdbpp
|
||||
requests
|
||||
|
||||
[testenv:.pkg]
|
||||
passenv =
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
|
||||
[testenv:auditwheels]
|
||||
skipsdist = True
|
||||
deps = auditwheel
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.61.0
|
||||
1.65.0
|
||||
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.61.0
|
||||
RUST_VERSION=1.65.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
101
src/authres.rs
101
src/authres.rs
@@ -34,10 +34,9 @@ pub(crate) async fn handle_authres(
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
warn!(context, "invalid email {:#}", e);
|
||||
// This email is invalid, but don't return an error, we still want to
|
||||
// add a stub to the database so that it's not downloaded again
|
||||
return Ok(DkimResults::default());
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +45,7 @@ pub(crate) async fn handle_authres(
|
||||
compute_dkim_results(context, authres, &from_domain, message_time).await
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
@@ -359,6 +358,7 @@ mod tests {
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::message;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
@@ -574,7 +574,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
assert!(res.allow_keychange);
|
||||
@@ -586,7 +586,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
if !res.allow_keychange {
|
||||
@@ -637,9 +637,10 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"Authentication-Results: dkim=";
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalidfrom.com", time())
|
||||
handle_authres(&t, &mail, "invalid@rom.com", time())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -681,7 +682,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
.await;
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
|
||||
let received = alice.recv_msg(&sent).await;
|
||||
|
||||
@@ -717,7 +718,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
loop {
|
||||
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
alice.recv_msg(&sent).await;
|
||||
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
bob2.recv_msg(&sent).await;
|
||||
@@ -750,4 +751,86 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let bob_alice_chat = bob.create_chat(&alice).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the peerstate should be accepted
|
||||
let sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
|
||||
assert!(peerstate.is_some());
|
||||
|
||||
// This also means that Bob can now write encrypted to Alice:
|
||||
let mut sent = bob
|
||||
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
|
||||
.await;
|
||||
assert!(sent.load_from_db().await.get_showpadlock());
|
||||
|
||||
// But if Bob writes to a mailing list, Alice doesn't show a padlock
|
||||
// since she can't verify the signature without accepting Bob's key:
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert!(!rcvd.get_showpadlock());
|
||||
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Assume Bob received an email from something@example.net with
|
||||
// correct DKIM -> `set_dkim_works()` was called
|
||||
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
|
||||
// And Bob knows his server's authserv-id
|
||||
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
|
||||
.await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.error.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the failed
|
||||
// authres isn't ignored
|
||||
let mut sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// Disallowing keychanges is disabled for now:
|
||||
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
|
||||
// The message info should contain a warning:
|
||||
assert!(message::get_msg_info(&bob, rcvd.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("KEYCHANGES NOT ALLOWED"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
130
src/chat.rs
130
src/chat.rs
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -81,6 +82,44 @@ impl Default for ProtectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
///
|
||||
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CantSendReason {
|
||||
/// Special chat.
|
||||
SpecialChat,
|
||||
|
||||
/// The chat is a device chat.
|
||||
DeviceChat,
|
||||
|
||||
/// The chat is a contact request, it needs to be accepted before sending a message.
|
||||
ContactRequest,
|
||||
|
||||
/// Mailing list without known List-Post header.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
}
|
||||
|
||||
impl fmt::Display for CantSendReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::SpecialChat => write!(f, "the chat is a special chat"),
|
||||
Self::DeviceChat => write!(f, "the chat is a device chat"),
|
||||
Self::ContactRequest => write!(
|
||||
f,
|
||||
"contact request chat should be accepted before sending messages"
|
||||
),
|
||||
Self::ReadOnlyMailingList => {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
Self::NotAMember => write!(f, "not a member of the chat"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chat ID, including reserved IDs.
|
||||
///
|
||||
/// Some chat IDs are reserved to identify special chat types. This
|
||||
@@ -645,8 +684,11 @@ impl ChatId {
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if !chat.can_send(context).await? {
|
||||
bail!("Can't set a draft: Can't send");
|
||||
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
|
||||
bail!(
|
||||
"Can't set a draft because chat is not writeable: {}",
|
||||
cant_send_reason
|
||||
);
|
||||
}
|
||||
|
||||
// set back draft information to allow identifying the draft later on -
|
||||
@@ -1082,14 +1124,33 @@ impl Chat {
|
||||
self.typ == Chattype::Mailinglist
|
||||
}
|
||||
|
||||
/// Returns true if user can send messages to this chat.
|
||||
/// Returns None if user can send messages to this chat.
|
||||
///
|
||||
/// Otherwise returns a reason useful for logging.
|
||||
pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
|
||||
use CantSendReason::*;
|
||||
|
||||
let reason = if self.id.is_special() {
|
||||
Some(SpecialChat)
|
||||
} else if self.is_device_talk() {
|
||||
Some(DeviceChat)
|
||||
} else if self.is_contact_request() {
|
||||
Some(ContactRequest)
|
||||
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
|
||||
Some(ReadOnlyMailingList)
|
||||
} else if !self.is_self_in_chat(context).await? {
|
||||
Some(NotAMember)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
/// Returns true if can send to the chat.
|
||||
///
|
||||
/// This function can be used by the UI to decide whether to display the input box.
|
||||
pub async fn can_send(&self, context: &Context) -> Result<bool> {
|
||||
let cannot_send = self.id.is_special()
|
||||
|| self.is_device_talk()
|
||||
|| self.is_contact_request()
|
||||
|| (self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty())
|
||||
|| !self.is_self_in_chat(context).await?;
|
||||
Ok(!cannot_send)
|
||||
Ok(self.why_cant_send(context).await?.is_none())
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
@@ -1255,18 +1316,13 @@ impl Chat {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
if !self.can_send(context).await? {
|
||||
if self.typ == Chattype::Group
|
||||
&& !is_contact_in_chat(context, self.id, ContactId::SELF).await?
|
||||
{
|
||||
if let Some(reason) = self.why_cant_send(context).await? {
|
||||
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot send message; self not in group.".into(),
|
||||
));
|
||||
bail!("Cannot set message; self not in group.");
|
||||
} else {
|
||||
error!(context, "Cannot send to chat type #{}.", self.typ,);
|
||||
bail!("Cannot send to chat type #{}", self.typ);
|
||||
}
|
||||
bail!("Cannot send message to {}: {}", self.id, reason);
|
||||
}
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
@@ -1881,7 +1937,9 @@ async fn prepare_msg_common(
|
||||
change_state_to: MessageState,
|
||||
) -> Result<MsgId> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
let update_msg_id = if msg.state == MessageState::OutDraft {
|
||||
@@ -2188,7 +2246,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
msg.text = Some(
|
||||
stock_str::videochat_invite_msg_body(context, Message::parse_webrtc_instance(&instance).1)
|
||||
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
|
||||
.await,
|
||||
);
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
@@ -2563,7 +2621,7 @@ pub async fn create_group_chat(
|
||||
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
||||
add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
|
||||
add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
@@ -2624,19 +2682,25 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Adds a contact to the `chats_contacts` table.
|
||||
/// Adds contacts to the `chats_contacts` table.
|
||||
pub(crate) async fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
contact_ids: &[ContactId],
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id],
|
||||
)
|
||||
.transaction(move |transaction| {
|
||||
for contact_id in contact_ids {
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2738,7 +2802,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if is_contact_in_chat(context, chat_id, contact_id).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
add_to_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
|
||||
}
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
@@ -3005,7 +3069,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
pub async fn set_chat_profile_image(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
new_image: impl AsRef<str>, // XXX use PathBuf
|
||||
new_image: &str, // XXX use PathBuf
|
||||
) -> Result<()> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
@@ -3023,13 +3087,12 @@ pub async fn set_chat_profile_image(
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param
|
||||
.set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
|
||||
if new_image.as_ref().is_empty() {
|
||||
if new_image.is_empty() {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
msg.param.remove(Param::Arg);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
|
||||
} else {
|
||||
let mut image_blob =
|
||||
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
|
||||
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
|
||||
image_blob.recode_to_avatar_size(context).await?;
|
||||
chat.param.set(Param::ProfileImage, image_blob.as_name());
|
||||
msg.param.set(Param::Arg, image_blob.as_name());
|
||||
@@ -3054,7 +3117,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
|
||||
let ids = context
|
||||
.sql
|
||||
@@ -4028,6 +4093,7 @@ mod tests {
|
||||
assert!(chat.is_device_talk());
|
||||
assert!(!chat.is_self_talk());
|
||||
assert!(!chat.can_send(&t).await?);
|
||||
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
|
||||
|
||||
assert_eq!(chat.name, stock_str::device_messages(&t).await);
|
||||
assert!(chat.get_profile_image(&t).await?.is_some());
|
||||
|
||||
@@ -194,20 +194,20 @@ pub enum Config {
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> Result<bool> {
|
||||
Ok(self.sql.get_raw_config(key).await?.is_some())
|
||||
Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
|
||||
}
|
||||
|
||||
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
|
||||
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key).await?,
|
||||
_ => self.sql.get_raw_config(key.as_ref()).await?,
|
||||
};
|
||||
|
||||
if value.is_some() {
|
||||
@@ -297,26 +297,30 @@ impl Context {
|
||||
Some(value) => {
|
||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key, None).await?;
|
||||
self.sql.set_raw_config(key.as_ref(), None).await?;
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(key, value).await;
|
||||
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
|
||||
// Interrupt ephemeral loop to delete old messages immediately.
|
||||
self.interrupt_ephemeral_task().await;
|
||||
ret?
|
||||
}
|
||||
Config::Displayname => {
|
||||
let value = value.map(improve_single_line_input);
|
||||
self.sql.set_raw_config(key, value.as_deref()).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), value.as_deref())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key, value).await?;
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Context {
|
||||
self,
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
format!("{:#}", err),
|
||||
&format!("{:#}", err),
|
||||
)
|
||||
.await
|
||||
)
|
||||
@@ -153,7 +153,7 @@ async fn on_configure_completed(
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text =
|
||||
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
|
||||
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.ok_or_log_msg(context, "Cannot add AEAP explanation");
|
||||
@@ -442,9 +442,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
// Send client ID as soon as possible before doing anything else.
|
||||
imap.determine_capabilities(ctx).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
imap.select_with_uidvalidity(ctx, "INBOX")
|
||||
|
||||
@@ -1578,7 +1578,6 @@ mod tests {
|
||||
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||
|
||||
@@ -2328,7 +2327,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Message is not encrypted.
|
||||
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(!message.get_showpadlock());
|
||||
|
||||
// Alice's second devices receives a copy of outgoing message.
|
||||
@@ -2355,7 +2354,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Second message is encrypted.
|
||||
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second devices receives a copy of second outgoing message.
|
||||
@@ -2410,7 +2409,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||
let message = sent_msg.load_from_db().await;
|
||||
assert!(message.get_showpadlock());
|
||||
|
||||
// Alice's second device receives a copy of the outgoing message.
|
||||
|
||||
@@ -614,9 +614,9 @@ impl Context {
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
|
||||
let server_id = self.server_id.read().await;
|
||||
res.insert("imap_server_id", format!("{:?}", server_id));
|
||||
drop(server_id);
|
||||
if let Some(server_id) = &*self.server_id.read().await {
|
||||
res.insert("imap_server_id", format!("{:?}", server_id));
|
||||
}
|
||||
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::authres::handle_authres;
|
||||
use crate::authres::{self, DkimResults};
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::log::LogExt;
|
||||
@@ -56,22 +56,44 @@ pub async fn try_decrypt(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn prepare_decryption(
|
||||
pub(crate) async fn prepare_decryption(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &[SingleInfo],
|
||||
from: &str,
|
||||
message_time: i64,
|
||||
is_thunderbird: bool,
|
||||
) -> Result<DecryptionInfo> {
|
||||
let from = if let Some(f) = from.first() {
|
||||
&f.addr
|
||||
} else {
|
||||
return Ok(DecryptionInfo::default());
|
||||
};
|
||||
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
|
||||
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring autocrypt header since this is a mailing list message. \
|
||||
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
|
||||
);
|
||||
}
|
||||
return Ok(DecryptionInfo {
|
||||
from: from.to_string(),
|
||||
autocrypt_header: None,
|
||||
peerstate: None,
|
||||
message_time,
|
||||
dkim_results: DkimResults {
|
||||
dkim_passed: false,
|
||||
dkim_should_work: false,
|
||||
allow_keychange: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
|
||||
let mut autocrypt_header = Aheader::from_headers(from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
|
||||
if is_thunderbird {
|
||||
if let Some(autocrypt_header) = &mut autocrypt_header {
|
||||
autocrypt_header.prefer_encrypt = EncryptPreference::Mutual;
|
||||
}
|
||||
}
|
||||
|
||||
let dkim_results = handle_authres(context, mail, from, message_time).await?;
|
||||
|
||||
let peerstate = get_autocrypt_peerstate(
|
||||
@@ -93,7 +115,7 @@ pub async fn prepare_decryption(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct DecryptionInfo {
|
||||
/// The From address. This is the address from the unnencrypted, outer
|
||||
/// From header.
|
||||
@@ -306,7 +328,7 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
if allow_change {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
@@ -322,7 +344,7 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
// to the database.
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -428,9 +428,7 @@ mod tests {
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
let sent2_rfc724_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
|
||||
|
||||
// not downloading the status update results in an placeholder
|
||||
receive_imf_inner(
|
||||
|
||||
@@ -147,7 +147,6 @@ mod tests {
|
||||
use crate::chat;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
use super::*;
|
||||
@@ -297,7 +296,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
vec![(Some(peerstate), addr)]
|
||||
|
||||
@@ -226,13 +226,13 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
0..=59 => {
|
||||
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
|
||||
stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
|
||||
}
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||
61..=3599 => {
|
||||
stock_str::msg_ephemeral_timer_minutes(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
||||
&format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
@@ -241,7 +241,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
3601..=86399 => {
|
||||
stock_str::msg_ephemeral_timer_hours(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
||||
&format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
@@ -250,7 +250,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
86401..=604_799 => {
|
||||
stock_str::msg_ephemeral_timer_days(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
||||
&format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
@@ -259,7 +259,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
_ => {
|
||||
stock_str::msg_ephemeral_timer_weeks(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
||||
&format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
||||
from_id,
|
||||
)
|
||||
.await
|
||||
|
||||
631
src/imap.rs
631
src/imap.rs
File diff suppressed because it is too large
Load Diff
26
src/imap/capabilities.rs
Normal file
26
src/imap/capabilities.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! # IMAP capabilities
|
||||
//!
|
||||
//! IMAP server capabilities are determined with a `CAPABILITY` command.
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Capabilities {
|
||||
/// True if the server has IDLE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc2177>
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc6851>
|
||||
pub can_move: bool,
|
||||
|
||||
/// True if the server has QUOTA capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc2087>
|
||||
pub can_check_quota: bool,
|
||||
|
||||
/// True if the server has CONDSTORE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc7162>
|
||||
pub can_condstore: bool,
|
||||
|
||||
/// Server ID if the server supports ID capability.
|
||||
pub server_id: Option<HashMap<String, String>>,
|
||||
}
|
||||
@@ -6,17 +6,21 @@ use std::{
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
|
||||
use async_smtp::ServerAddress;
|
||||
use tokio::net::{self, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
use crate::login_param::{build_tls, Socks5Config};
|
||||
|
||||
use super::session::SessionStream;
|
||||
|
||||
/// IMAP write and read timeout in seconds.
|
||||
const IMAP_TIMEOUT: u64 = 30;
|
||||
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
@@ -38,27 +42,54 @@ impl DerefMut for Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine server capabilities.
|
||||
///
|
||||
/// If server supports ID capability, send our client ID.
|
||||
async fn determine_capabilities(
|
||||
session: &mut ImapSession<Box<dyn SessionStream>>,
|
||||
) -> Result<Capabilities> {
|
||||
let caps = session
|
||||
.capabilities()
|
||||
.await
|
||||
.context("CAPABILITY command error")?;
|
||||
let server_id = if caps.has_str("ID") {
|
||||
session.id([("name", Some("Delta Chat"))]).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let capabilities = Capabilities {
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
server_id,
|
||||
};
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn login(self, username: &str, password: &str) -> Result<Session> {
|
||||
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
|
||||
let Client { inner, .. } = self;
|
||||
let session = inner
|
||||
let mut session = inner
|
||||
.login(username, password)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
Ok(Session { inner: session })
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
Ok(Session::new(session, capabilities))
|
||||
}
|
||||
|
||||
pub async fn authenticate(
|
||||
pub(crate) async fn authenticate(
|
||||
self,
|
||||
auth_type: &str,
|
||||
authenticator: impl async_imap::Authenticator,
|
||||
) -> Result<Session> {
|
||||
let Client { inner, .. } = self;
|
||||
let session = inner
|
||||
let mut session = inner
|
||||
.authenticate(auth_type, authenticator)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
Ok(Session { inner: session })
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
Ok(Session::new(session, capabilities))
|
||||
}
|
||||
|
||||
pub async fn connect_secure(
|
||||
@@ -66,9 +97,15 @@ impl Client {
|
||||
domain: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
|
||||
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
let timeout_stream = Box::pin(timeout_stream);
|
||||
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(domain, timeout_stream).await?);
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
|
||||
let _greeting = client
|
||||
@@ -83,7 +120,12 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
|
||||
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
|
||||
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
let timeout_stream = Box::pin(timeout_stream);
|
||||
let stream: Box<dyn SessionStream> = Box::new(timeout_stream);
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
let _greeting = client
|
||||
@@ -104,7 +146,7 @@ impl Client {
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> = Box::new(
|
||||
socks5_config
|
||||
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
|
||||
.connect(target_addr, Some(IMAP_TIMEOUT))
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -130,7 +172,7 @@ impl Client {
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> = Box::new(
|
||||
socks5_config
|
||||
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
|
||||
.connect(target_addr, Some(IMAP_TIMEOUT))
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -146,7 +188,7 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Client> {
|
||||
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Self> {
|
||||
if self.is_secure {
|
||||
Ok(self)
|
||||
} else {
|
||||
|
||||
183
src/imap/idle.rs
183
src/imap/idle.rs
@@ -1,113 +1,114 @@
|
||||
use super::Imap;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use futures_lite::FutureExt;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::session::Session;
|
||||
use crate::imap::client::IMAP_TIMEOUT;
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
use super::session::Session;
|
||||
|
||||
impl Imap {
|
||||
pub fn can_idle(&self) -> bool {
|
||||
self.config.can_idle
|
||||
}
|
||||
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
|
||||
|
||||
impl Session {
|
||||
pub async fn idle(
|
||||
&mut self,
|
||||
mut self,
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
watch_folder: Option<String>,
|
||||
) -> Result<InterruptInfo> {
|
||||
) -> Result<(Self, InterruptInfo)> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
self.prepare(context).await?;
|
||||
|
||||
let mut info = Default::default();
|
||||
|
||||
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
return Ok(info);
|
||||
return Ok((self, info));
|
||||
}
|
||||
|
||||
if let Some(session) = self.session.take() {
|
||||
if let Ok(info) = self.idle_interrupt.try_recv() {
|
||||
info!(context, "skip idle, got interrupt {:?}", info);
|
||||
self.session = Some(session);
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
|
||||
enum Event {
|
||||
IdleResponse(IdleResponse),
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
let folder_name = watch_folder.as_deref().unwrap_or("None");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle entering wait-on-remote state", folder_name
|
||||
);
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let info = self.idle_interrupt.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
drop(interrupt);
|
||||
|
||||
Ok(Event::Interrupt(info.unwrap_or_default()))
|
||||
});
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle-wait timeout or interruption", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted manually", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted: {:?}", folder_name, &i
|
||||
);
|
||||
info = i;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
|
||||
}
|
||||
}
|
||||
|
||||
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
|
||||
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
if let Ok(info) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "skip idle, got interrupt {:?}", info);
|
||||
return Ok((self, info));
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
let mut handle = self.inner.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
|
||||
}
|
||||
|
||||
// At this point IDLE command was sent and we received a "+ idling" response. We will now
|
||||
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
|
||||
// disable read timeout, we would get a timeout after `IMAP_TIMEOUT`, which is a lot
|
||||
// shorter than `IDLE_TIMEOUT`.
|
||||
handle.as_mut().set_read_timeout(None);
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
|
||||
|
||||
enum Event {
|
||||
IdleResponse(IdleResponse),
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
let folder_name = watch_folder.as_deref().unwrap_or("None");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle entering wait-on-remote state", folder_name
|
||||
);
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let info = idle_interrupt_receiver.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
drop(interrupt);
|
||||
|
||||
Ok(Event::Interrupt(info.unwrap_or_default()))
|
||||
});
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle-wait timeout or interruption", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted manually", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted: {:?}", folder_name, &i
|
||||
);
|
||||
info = i;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
|
||||
}
|
||||
}
|
||||
|
||||
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
|
||||
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
|
||||
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
self.inner = session;
|
||||
|
||||
Ok((self, info))
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub(crate) async fn fake_idle(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -123,7 +124,11 @@ impl Imap {
|
||||
watch_folder
|
||||
} else {
|
||||
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
|
||||
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||
return self
|
||||
.idle_interrupt_receiver
|
||||
.recv()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
};
|
||||
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
|
||||
|
||||
@@ -142,7 +147,7 @@ impl Imap {
|
||||
.tick()
|
||||
.map(|_| Event::Tick)
|
||||
.race(
|
||||
self.idle_interrupt
|
||||
self.idle_interrupt_receiver
|
||||
.recv()
|
||||
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
|
||||
)
|
||||
@@ -156,9 +161,11 @@ impl Imap {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false);
|
||||
if let Some(session) = &self.session {
|
||||
if session.can_idle() {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
@@ -177,7 +184,7 @@ impl Imap {
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {:#}", err);
|
||||
self.trigger_reconnect(context).await;
|
||||
self.trigger_reconnect(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +63,18 @@ impl Imap {
|
||||
|
||||
// Don't scan folders that are watched anyway
|
||||
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
|
||||
let session = self.session.as_mut().context("no session")?;
|
||||
// Drain leftover unsolicited EXISTS messages
|
||||
self.server_sent_unsolicited_exists(context)?;
|
||||
session.server_sent_unsolicited_exists(context)?;
|
||||
|
||||
loop {
|
||||
self.fetch_move_delete(context, folder.name(), is_spam_folder)
|
||||
.await
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
|
||||
let session = self.session.as_mut().context("no session")?;
|
||||
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
|
||||
if !self.server_sent_unsolicited_exists(context)? {
|
||||
if !session.server_sent_unsolicited_exists(context)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::Imap;
|
||||
use super::session::Session as ImapSession;
|
||||
|
||||
use crate::context::Context;
|
||||
use anyhow::Context as _;
|
||||
@@ -7,9 +7,6 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP Could not obtain imap-session object.")]
|
||||
NoSession,
|
||||
|
||||
#[error("IMAP Connection Lost or no connection established")]
|
||||
ConnectionLost,
|
||||
|
||||
@@ -29,55 +26,38 @@ impl From<anyhow::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
/// Issues a CLOSE command to expunge selected folder.
|
||||
impl ImapSession {
|
||||
/// Issues a CLOSE command if selected folder needs expunge,
|
||||
/// i.e. if Delta Chat marked a message there as deleted previously.
|
||||
///
|
||||
/// CLOSE is considerably faster than an EXPUNGE, see
|
||||
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
|
||||
pub(super) async fn close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Some(ref folder) = self.config.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Some(folder) = &self.selected_folder {
|
||||
if self.selected_folder_needs_expunge {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
let session = self.session.as_mut().context("no session")?;
|
||||
if let Err(err) = session.close().await.context("IMAP close/expunge failed") {
|
||||
self.trigger_reconnect(context).await;
|
||||
return Err(err);
|
||||
self.close().await.context("IMAP close/expunge failed")?;
|
||||
info!(context, "close/expunge succeeded");
|
||||
self.selected_folder = None;
|
||||
self.selected_folder_needs_expunge = false;
|
||||
}
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Issues a CLOSE command if selected folder needs expunge.
|
||||
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if self.config.selected_folder_needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
/// Selects a folder, possibly updating uid_validity and, if needed,
|
||||
/// expunging the folder to remove delete-marked messages.
|
||||
/// Returns whether a new folder was selected.
|
||||
pub(super) async fn select_folder(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: Option<&str>,
|
||||
) -> Result<NewlySelected> {
|
||||
if self.session.is_none() {
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
self.trigger_reconnect(context).await;
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(folder) = folder {
|
||||
if let Some(ref selected_folder) = self.config.selected_folder {
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
@@ -89,42 +69,30 @@ impl Imap {
|
||||
|
||||
// select new folder
|
||||
if let Some(folder) = folder {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
let res = if self.config.can_condstore {
|
||||
session.select_condstore(folder).await
|
||||
} else {
|
||||
session.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.config.selected_folder = Some(folder.to_string());
|
||||
self.config.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => {
|
||||
self.trigger_reconnect(context).await;
|
||||
self.config.selected_folder = None;
|
||||
Err(Error::ConnectionLost)
|
||||
}
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => {
|
||||
self.config.selected_folder = None;
|
||||
self.trigger_reconnect(context).await;
|
||||
Err(Error::Other(err.to_string()))
|
||||
}
|
||||
}
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
Err(Error::NoSession)
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => Err(Error::Other(err.to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(NewlySelected::No)
|
||||
@@ -141,8 +109,7 @@ impl Imap {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => {
|
||||
let session = self.session.as_mut().context("no IMAP session")?;
|
||||
session.create(folder).await.with_context(|| {
|
||||
self.create(folder).await.with_context(|| {
|
||||
format!("Couldn't select folder ('{}'), then create() failed", err)
|
||||
})?;
|
||||
|
||||
@@ -153,6 +120,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
|
||||
pub(super) enum NewlySelected {
|
||||
/// The folder was newly selected during this call to select_folder().
|
||||
|
||||
@@ -1,24 +1,59 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_imap::types::Mailbox;
|
||||
use async_imap::Session as ImapSession;
|
||||
use async_native_tls::TlsStream;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Session {
|
||||
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
|
||||
|
||||
pub capabilities: Capabilities,
|
||||
|
||||
/// Selected folder name.
|
||||
pub selected_folder: Option<String>,
|
||||
|
||||
/// Mailbox structure returned by IMAP server.
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
}
|
||||
|
||||
pub(crate) trait SessionStream:
|
||||
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + std::fmt::Debug
|
||||
{
|
||||
/// Change the read timeout on the session stream.
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
}
|
||||
|
||||
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
|
||||
impl SessionStream for TlsStream<TcpStream> {}
|
||||
impl SessionStream for TcpStream {}
|
||||
impl SessionStream for Socks5Stream<TcpStream> {}
|
||||
impl SessionStream for TlsStream<Box<dyn SessionStream>> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
}
|
||||
impl SessionStream for TlsStream<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
}
|
||||
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout_pinned(timeout);
|
||||
}
|
||||
}
|
||||
impl SessionStream for Socks5Stream<TcpStream> {
|
||||
fn set_read_timeout(&mut self, _timeout: Option<Duration>) {
|
||||
// FIXME: build SOCKS streams on top of TimeoutStream, not directly TcpStream,
|
||||
// so we can set a read timeout for them.
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
type Target = ImapSession<Box<dyn SessionStream>>;
|
||||
@@ -35,8 +70,32 @@ impl DerefMut for Session {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
|
||||
let Session { inner } = self;
|
||||
inner.idle()
|
||||
pub(crate) fn new(
|
||||
inner: ImapSession<Box<dyn SessionStream>>,
|
||||
capabilities: Capabilities,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
capabilities,
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_idle(&self) -> bool {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
self.capabilities.can_move
|
||||
}
|
||||
|
||||
pub fn can_check_quota(&self) -> bool {
|
||||
self.capabilities.can_check_quota
|
||||
}
|
||||
|
||||
pub fn can_condstore(&self) -> bool {
|
||||
self.capabilities.can_condstore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl LoginParam {
|
||||
async fn from_database(context: &Context, prefix: &str) -> Result<Self> {
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
let key = &format!("{}addr", prefix);
|
||||
let addr = sql
|
||||
.get_raw_config(key)
|
||||
.await?
|
||||
@@ -177,26 +177,26 @@ impl LoginParam {
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
let key = &format!("{}mail_server", prefix);
|
||||
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
let key = &format!("{}mail_port", prefix);
|
||||
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
let key = &format!("{}mail_user", prefix);
|
||||
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let key = &format!("{}mail_pw", prefix);
|
||||
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
let key = &format!("{}mail_security", prefix);
|
||||
let mail_security = sql
|
||||
.get_raw_config_int(key)
|
||||
.await?
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let key = &format!("{}imap_certificate_checks", prefix);
|
||||
let imap_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
@@ -204,26 +204,26 @@ impl LoginParam {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
let key = &format!("{}send_server", prefix);
|
||||
let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
let key = &format!("{}send_port", prefix);
|
||||
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
let key = &format!("{}send_user", prefix);
|
||||
let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let key = &format!("{}send_pw", prefix);
|
||||
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
let key = &format!("{}send_security", prefix);
|
||||
let send_security = sql
|
||||
.get_raw_config_int(key)
|
||||
.await?
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let key = &format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default()
|
||||
@@ -231,11 +231,11 @@ impl LoginParam {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
let key = &format!("{}server_flags", prefix);
|
||||
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
|
||||
let key = format!("{}provider", prefix);
|
||||
let key = &format!("{}provider", prefix);
|
||||
let provider = sql
|
||||
.get_raw_config(key)
|
||||
.await?
|
||||
@@ -275,50 +275,50 @@ impl LoginParam {
|
||||
|
||||
context.set_primary_self_addr(&self.addr).await?;
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
let key = &format!("{}mail_server", prefix);
|
||||
sql.set_raw_config(key, Some(&self.imap.server)).await?;
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
let key = &format!("{}mail_port", prefix);
|
||||
sql.set_raw_config_int(key, i32::from(self.imap.port))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
let key = &format!("{}mail_user", prefix);
|
||||
sql.set_raw_config(key, Some(&self.imap.user)).await?;
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let key = &format!("{}mail_pw", prefix);
|
||||
sql.set_raw_config(key, Some(&self.imap.password)).await?;
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
let key = &format!("{}mail_security", prefix);
|
||||
sql.set_raw_config_int(key, self.imap.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let key = &format!("{}imap_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
let key = &format!("{}send_server", prefix);
|
||||
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
let key = &format!("{}send_port", prefix);
|
||||
sql.set_raw_config_int(key, i32::from(self.smtp.port))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
let key = &format!("{}send_user", prefix);
|
||||
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let key = &format!("{}send_pw", prefix);
|
||||
sql.set_raw_config(key, Some(&self.smtp.password)).await?;
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
let key = &format!("{}send_security", prefix);
|
||||
sql.set_raw_config_int(key, self.smtp.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let key = &format!("{}smtp_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
// The OAuth2 flag is either set for both IMAP and SMTP or not at all.
|
||||
let key = format!("{}server_flags", prefix);
|
||||
let key = &format!("{}server_flags", prefix);
|
||||
let server_flags = match self.imap.oauth2 {
|
||||
true => DC_LP_AUTH_OAUTH2,
|
||||
false => DC_LP_AUTH_NORMAL,
|
||||
@@ -326,7 +326,7 @@ impl LoginParam {
|
||||
sql.set_raw_config_int(key, server_flags).await?;
|
||||
|
||||
if let Some(provider) = self.provider {
|
||||
let key = format!("{}provider", prefix);
|
||||
let key = &format!("{}provider", prefix);
|
||||
sql.set_raw_config(key, Some(provider.id)).await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let self_name = match context.get_config(Config::Displayname).await? {
|
||||
let self_name = &match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
};
|
||||
@@ -1259,7 +1259,7 @@ impl<'a> MimeFactory<'a> {
|
||||
.truncated_text(32)
|
||||
.to_string()
|
||||
};
|
||||
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
|
||||
let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
|
||||
let message_text = format!("{}\r\n", format_flowed(&p2));
|
||||
message = message.child(
|
||||
PartBuilder::new()
|
||||
@@ -1733,11 +1733,11 @@ mod tests {
|
||||
}
|
||||
async fn get_subject(
|
||||
t: &TestContext,
|
||||
sent: crate::test_utils::SentMessage,
|
||||
sent: crate::test_utils::SentMessage<'_>,
|
||||
) -> Result<String> {
|
||||
let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap();
|
||||
|
||||
let sent_msg = Message::load_from_db(t, sent.sender_msg_id).await?;
|
||||
let sent_msg = sent.load_from_db().await;
|
||||
assert_eq!(parsed_subject, sent_msg.subject);
|
||||
|
||||
Ok(parsed_subject)
|
||||
|
||||
@@ -21,7 +21,6 @@ use crate::events::EventType;
|
||||
use crate::format_flowed::unformat_flowed;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::Fingerprint;
|
||||
use crate::location;
|
||||
use crate::message::{self, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -29,6 +28,7 @@ use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||
use crate::{location, tools};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -46,7 +46,7 @@ pub struct MimeMessage {
|
||||
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub from: SingleInfo,
|
||||
/// Whether the From address was repeated in the signed part
|
||||
/// (and we know that the signer intended to send from this address)
|
||||
pub from_is_signed: bool,
|
||||
@@ -166,7 +166,7 @@ impl MimeMessage {
|
||||
///
|
||||
/// If `partial` is set, it contains the full message size in bytes
|
||||
/// and `body` contains the header only.
|
||||
pub async fn from_bytes_with_partial(
|
||||
pub(crate) async fn from_bytes_with_partial(
|
||||
context: &Context,
|
||||
body: &[u8],
|
||||
partial: Option<u32>,
|
||||
@@ -216,11 +216,21 @@ impl MimeMessage {
|
||||
headers.remove("secure-join-fingerprint");
|
||||
headers.remove("chat-verified");
|
||||
|
||||
let is_thunderbird = headers
|
||||
.get("user-agent")
|
||||
.map_or(false, |user_agent| user_agent.contains("Thunderbird"));
|
||||
if is_thunderbird {
|
||||
info!(context, "Detected Thunderbird");
|
||||
}
|
||||
|
||||
let from = from.context("No from in message")?;
|
||||
let mut decryption_info =
|
||||
prepare_decryption(context, &mail, &from.addr, message_time, is_thunderbird).await?;
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossiped_addr = Default::default();
|
||||
let mut from_is_signed = false;
|
||||
let mut decryption_info = prepare_decryption(context, &mail, &from, message_time).await?;
|
||||
hop_info += "\n\n";
|
||||
hop_info += &decryption_info.dkim_results.to_string();
|
||||
|
||||
@@ -255,7 +265,7 @@ impl MimeMessage {
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut signed_from = Vec::new();
|
||||
let mut signed_from = None;
|
||||
|
||||
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
|
||||
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
@@ -270,23 +280,21 @@ impl MimeMessage {
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
if let Some(signed_from) = signed_from.first() {
|
||||
if let Some(from) = from.first() {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one"
|
||||
);
|
||||
}
|
||||
if let Some(signed_from) = signed_from {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +310,7 @@ impl MimeMessage {
|
||||
// && decryption_info.dkim_results.allow_keychange
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
(Ok(mail), HashSet::new(), false)
|
||||
@@ -582,21 +590,18 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed && !self.parts.is_empty() {
|
||||
if let Some(ref dn_to) = self.chat_disposition_notification_to {
|
||||
if let Some(from) = self.from.get(0) {
|
||||
// Check that the message is not outgoing.
|
||||
if !context.is_self_addr(&from.addr).await? {
|
||||
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring",
|
||||
from.addr,
|
||||
dn_to.addr
|
||||
);
|
||||
// Check that the message is not outgoing.
|
||||
let from = &self.from.addr;
|
||||
if !context.is_self_addr(from).await? {
|
||||
if from.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1226,7 +1231,7 @@ impl MimeMessage {
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
from: &mut Vec<SingleInfo>,
|
||||
from: &mut Option<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
@@ -1255,7 +1260,7 @@ impl MimeMessage {
|
||||
*recipients = recipients_new;
|
||||
}
|
||||
let from_new = get_from(fields);
|
||||
if !from_new.is_empty() {
|
||||
if from_new.is_some() {
|
||||
*from = from_new;
|
||||
}
|
||||
let list_post_new = get_list_post(fields);
|
||||
@@ -1581,11 +1586,11 @@ async fn update_gossip_peerstates(
|
||||
let peerstate;
|
||||
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
|
||||
p.apply_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql, false).await?;
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = p;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(&header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
p.save_to_db(&context.sql).await?;
|
||||
peerstate = p;
|
||||
};
|
||||
peerstate
|
||||
@@ -1800,8 +1805,9 @@ pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
|
||||
let all = get_all_addresses_from_header(headers, |header_key| header_key == "from");
|
||||
tools::single_value(all)
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
@@ -1877,35 +1883,35 @@ mod tests {
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
@@ -1913,7 +1919,7 @@ mod tests {
|
||||
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
|
||||
@@ -1923,7 +1929,7 @@ mod tests {
|
||||
MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
}
|
||||
@@ -2155,14 +2161,9 @@ mod tests {
|
||||
test1\n\
|
||||
";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
|
||||
|
||||
let of = &mimeparser.from[0];
|
||||
assert_eq!(of.addr, "hello@one.org");
|
||||
|
||||
assert!(mimeparser.chat_disposition_notification_to.is_none());
|
||||
assert!(mimeparser.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2201,7 +2202,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mimeparser_with_context() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"From: hello\n\
|
||||
let raw = b"From: hello@example.org\n\
|
||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||
Subject: outer-subject\n\
|
||||
Secure-Join-Group: no\n\
|
||||
|
||||
210
src/peerstate.rs
210
src/peerstate.rs
@@ -4,7 +4,7 @@ use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::chat::{self, is_contact_in_chat, Chat};
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{addr_cmp, Contact, Origin};
|
||||
@@ -46,7 +46,6 @@ pub struct Peerstate {
|
||||
pub gossip_key_fingerprint: Option<Fingerprint>,
|
||||
pub verified_key: Option<SignedPublicKey>,
|
||||
pub verified_key_fingerprint: Option<Fingerprint>,
|
||||
pub to_save: Option<ToSave>,
|
||||
pub fingerprint_changed: bool,
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ impl PartialEq for Peerstate {
|
||||
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
|
||||
&& self.verified_key == other.verified_key
|
||||
&& self.verified_key_fingerprint == other.verified_key_fingerprint
|
||||
&& self.to_save == other.to_save
|
||||
&& self.fingerprint_changed == other.fingerprint_changed
|
||||
}
|
||||
}
|
||||
@@ -84,19 +82,11 @@ impl fmt::Debug for Peerstate {
|
||||
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
|
||||
.field("verified_key", &self.verified_key)
|
||||
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
|
||||
.field("to_save", &self.to_save)
|
||||
.field("fingerprint_changed", &self.fingerprint_changed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ToSave {
|
||||
Timestamps = 0x01,
|
||||
All = 0x02,
|
||||
}
|
||||
|
||||
impl Peerstate {
|
||||
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
|
||||
Peerstate {
|
||||
@@ -111,7 +101,6 @@ impl Peerstate {
|
||||
gossip_timestamp: 0,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
}
|
||||
}
|
||||
@@ -137,7 +126,6 @@ impl Peerstate {
|
||||
gossip_timestamp: message_time,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
}
|
||||
}
|
||||
@@ -229,7 +217,6 @@ impl Peerstate {
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
@@ -248,14 +235,10 @@ impl Peerstate {
|
||||
let old_public_fingerprint = self.public_key_fingerprint.take();
|
||||
self.public_key_fingerprint = Some(public_key.fingerprint());
|
||||
|
||||
if old_public_fingerprint.is_none()
|
||||
|| self.public_key_fingerprint.is_none()
|
||||
|| old_public_fingerprint != self.public_key_fingerprint
|
||||
if old_public_fingerprint.is_some()
|
||||
&& old_public_fingerprint != self.public_key_fingerprint
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
if old_public_fingerprint.is_some() {
|
||||
self.fingerprint_changed = true;
|
||||
}
|
||||
self.fingerprint_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +250,6 @@ impl Peerstate {
|
||||
|| self.gossip_key_fingerprint.is_none()
|
||||
|| old_gossip_fingerprint != self.gossip_key_fingerprint
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
|
||||
// Warn about gossip key change only if there is no public key obtained from
|
||||
// Autocrypt header, which overrides gossip key.
|
||||
if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
|
||||
@@ -281,7 +262,6 @@ impl Peerstate {
|
||||
pub fn degrade_encryption(&mut self, message_time: i64) {
|
||||
self.prefer_encrypt = EncryptPreference::Reset;
|
||||
self.last_seen = message_time;
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
|
||||
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
|
||||
@@ -292,19 +272,16 @@ impl Peerstate {
|
||||
if message_time > self.last_seen {
|
||||
self.last_seen = message_time;
|
||||
self.last_seen_autocrypt = message_time;
|
||||
self.to_save = Some(ToSave::Timestamps);
|
||||
if (header.prefer_encrypt == EncryptPreference::Mutual
|
||||
|| header.prefer_encrypt == EncryptPreference::NoPreference)
|
||||
&& header.prefer_encrypt != self.prefer_encrypt
|
||||
{
|
||||
self.prefer_encrypt = header.prefer_encrypt;
|
||||
self.to_save = Some(ToSave::All)
|
||||
}
|
||||
|
||||
if self.public_key.as_ref() != Some(&header.public_key) {
|
||||
self.public_key = Some(header.public_key.clone());
|
||||
self.recalc_fingerprint();
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,11 +293,9 @@ impl Peerstate {
|
||||
|
||||
if message_time > self.gossip_timestamp {
|
||||
self.gossip_timestamp = message_time;
|
||||
self.to_save = Some(ToSave::Timestamps);
|
||||
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
|
||||
self.gossip_key = Some(gossip_header.public_key.clone());
|
||||
self.recalc_fingerprint();
|
||||
self.to_save = Some(ToSave::All)
|
||||
}
|
||||
|
||||
// This is non-standard.
|
||||
@@ -339,7 +314,6 @@ impl Peerstate {
|
||||
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual
|
||||
{
|
||||
self.prefer_encrypt = EncryptPreference::Mutual;
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -395,7 +369,6 @@ impl Peerstate {
|
||||
if self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == fingerprint
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
self.verified_key = self.public_key.clone();
|
||||
self.verified_key_fingerprint = self.public_key_fingerprint.clone();
|
||||
true
|
||||
@@ -407,7 +380,6 @@ impl Peerstate {
|
||||
if self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
self.verified_key = self.gossip_key.clone();
|
||||
self.verified_key_fingerprint = self.gossip_key_fingerprint.clone();
|
||||
true
|
||||
@@ -421,66 +393,48 @@ impl Peerstate {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> Result<()> {
|
||||
if self.to_save == Some(ToSave::All) || create {
|
||||
sql.execute(
|
||||
if create {
|
||||
"INSERT INTO acpeerstates ( \
|
||||
last_seen, \
|
||||
last_seen_autocrypt, \
|
||||
prefer_encrypted, \
|
||||
public_key, \
|
||||
gossip_timestamp, \
|
||||
gossip_key, \
|
||||
public_key_fingerprint, \
|
||||
gossip_key_fingerprint, \
|
||||
verified_key, \
|
||||
verified_key_fingerprint, \
|
||||
addr \
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
||||
} else {
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, \
|
||||
last_seen_autocrypt=?, \
|
||||
prefer_encrypted=?, \
|
||||
public_key=?, \
|
||||
gossip_timestamp=?, \
|
||||
gossip_key=?, \
|
||||
public_key_fingerprint=?, \
|
||||
gossip_key_fingerprint=?, \
|
||||
verified_key=?, \
|
||||
verified_key_fingerprint=? \
|
||||
WHERE addr=?"
|
||||
},
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
self.public_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.gossip_timestamp,
|
||||
self.gossip_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.addr,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
} else if self.to_save == Some(ToSave::Timestamps) {
|
||||
sql.execute(
|
||||
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
||||
WHERE addr=?;",
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.gossip_timestamp,
|
||||
self.addr
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
|
||||
sql.execute(
|
||||
"INSERT INTO acpeerstates (
|
||||
last_seen,
|
||||
last_seen_autocrypt,
|
||||
prefer_encrypted,
|
||||
public_key,
|
||||
gossip_timestamp,
|
||||
gossip_key,
|
||||
public_key_fingerprint,
|
||||
gossip_key_fingerprint,
|
||||
verified_key,
|
||||
verified_key_fingerprint,
|
||||
addr)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET
|
||||
last_seen = excluded.last_seen,
|
||||
last_seen_autocrypt = excluded.last_seen_autocrypt,
|
||||
prefer_encrypted = excluded.prefer_encrypted,
|
||||
public_key = excluded.public_key,
|
||||
gossip_timestamp = excluded.gossip_timestamp,
|
||||
gossip_key = excluded.gossip_key,
|
||||
public_key_fingerprint = excluded.public_key_fingerprint,
|
||||
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
|
||||
verified_key = excluded.verified_key,
|
||||
verified_key_fingerprint = excluded.verified_key_fingerprint",
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
self.public_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.gossip_timestamp,
|
||||
self.gossip_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.addr,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -520,7 +474,7 @@ impl Peerstate {
|
||||
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
|
||||
let msg = match &change {
|
||||
PeerstateChange::FingerprintChange => {
|
||||
stock_str::contact_setup_changed(context, self.addr.clone()).await
|
||||
stock_str::contact_setup_changed(context, &self.addr).await
|
||||
}
|
||||
PeerstateChange::Aeap(new_addr) => {
|
||||
let old_contact = Contact::load_from_db(context, contact_id).await?;
|
||||
@@ -569,9 +523,7 @@ impl Peerstate {
|
||||
let (new_contact_id, _) =
|
||||
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
|
||||
.await?;
|
||||
if !is_contact_in_chat(context, *chat_id, new_contact_id).await? {
|
||||
chat::add_to_chat_contacts_table(context, *chat_id, new_contact_id).await?;
|
||||
}
|
||||
chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(*chat_id));
|
||||
}
|
||||
@@ -617,10 +569,9 @@ pub async fn maybe_do_aeap_transition(
|
||||
mime_parser: &crate::mimeparser::MimeMessage,
|
||||
) -> Result<()> {
|
||||
if let Some(peerstate) = &mut info.peerstate {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &from.addr)
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
@@ -636,31 +587,24 @@ pub async fn maybe_do_aeap_transition(
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
&& mime_parser.from_is_signed
|
||||
&& info.message_time > peerstate.last_seen
|
||||
{
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
context,
|
||||
info.message_time,
|
||||
PeerstateChange::Aeap(info.from.clone()),
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
context,
|
||||
info.message_time,
|
||||
PeerstateChange::Aeap(info.from.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
peerstate.addr = info.from.clone();
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
peerstate.addr = info.from.clone();
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
|
||||
// We don't know whether a peerstate with this address already existed, or a
|
||||
// new one should be created, so just try both create=false and create=true,
|
||||
// and if this fails, create=true, one will succeed (this is a very cold path,
|
||||
// so performance doesn't really matter).
|
||||
peerstate.save_to_db(&context.sql, true).await?;
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +656,7 @@ mod tests {
|
||||
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
let peerstate = Peerstate {
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -724,12 +668,11 @@ mod tests {
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save to db"
|
||||
);
|
||||
|
||||
@@ -738,8 +681,6 @@ mod tests {
|
||||
.expect("failed to load peerstate from db")
|
||||
.expect("no peerstate found in the database");
|
||||
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
|
||||
.await
|
||||
@@ -766,16 +707,15 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save"
|
||||
);
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"double-call with create failed"
|
||||
);
|
||||
}
|
||||
@@ -787,7 +727,7 @@ mod tests {
|
||||
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
let peerstate = Peerstate {
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -799,12 +739,11 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save"
|
||||
);
|
||||
|
||||
@@ -812,8 +751,6 @@ mod tests {
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(Some(peerstate), peerstate_new);
|
||||
}
|
||||
|
||||
@@ -864,7 +801,6 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
|
||||
@@ -641,7 +641,6 @@ mod tests {
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::key::DcKey;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
use anyhow::Result;
|
||||
@@ -894,11 +893,10 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save peerstate"
|
||||
);
|
||||
|
||||
|
||||
10
src/quota.rs
10
src/quota.rs
@@ -8,6 +8,7 @@ use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::scan_folders::get_watched_folders;
|
||||
use crate::imap::session::Session as ImapSession;
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{Action, Status};
|
||||
use crate::message::{Message, Viewtype};
|
||||
@@ -49,12 +50,12 @@ pub struct QuotaInfo {
|
||||
}
|
||||
|
||||
async fn get_unique_quota_roots_and_usage(
|
||||
session: &mut ImapSession,
|
||||
folders: Vec<String>,
|
||||
imap: &mut Imap,
|
||||
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
|
||||
for folder in folders {
|
||||
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
|
||||
let (quota_roots, quotas) = &session.get_quota_root(&folder).await?;
|
||||
// if there are new quota roots found in this imap folder, add them to the list
|
||||
for qr_entries in quota_roots {
|
||||
for quota_root_name in &qr_entries.quota_root_names {
|
||||
@@ -135,9 +136,10 @@ impl Context {
|
||||
return Ok(Status::RetryNow);
|
||||
}
|
||||
|
||||
let quota = if imap.can_check_quota() {
|
||||
let session = imap.session.as_mut().context("no session")?;
|
||||
let quota = if session.can_check_quota() {
|
||||
let folders = get_watched_folders(self).await?;
|
||||
get_unique_quota_roots_and_usage(folders, imap).await
|
||||
get_unique_quota_roots_and_usage(session, folders).await
|
||||
} else {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ use regex::Regex;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact;
|
||||
use crate::contact::{
|
||||
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
|
||||
};
|
||||
@@ -22,14 +21,14 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{
|
||||
parse_message_id, parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
|
||||
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
@@ -37,7 +36,8 @@ use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{create_id, extract_grpid_from_rfc724_mid, smeared_time};
|
||||
use crate::tools::{extract_grpid_from_rfc724_mid, smeared_time};
|
||||
use crate::{contact, imap};
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
///
|
||||
@@ -66,25 +66,19 @@ pub async fn receive_imf(
|
||||
seen: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
let mail = parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let rfc724_mid = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::MessageId)
|
||||
.and_then(|msgid| parse_message_id(&msgid).ok())
|
||||
.unwrap_or_else(create_id);
|
||||
let rfc724_mid = imap::prefetch_get_or_create_message_id(&mail.headers);
|
||||
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
|
||||
}
|
||||
|
||||
/// Receive a message and add it to the database.
|
||||
///
|
||||
/// Returns an error on recoverable errors, e.g. database errors. In this case,
|
||||
/// message parsing should be retried later.
|
||||
/// Returns an error on database failure or if the message is broken,
|
||||
/// e.g. has nonstandard MIME structure.
|
||||
///
|
||||
/// If message itself is wrong, logs
|
||||
/// the error and returns success:
|
||||
/// - If possible, creates a database entry to prevent the message from being
|
||||
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`
|
||||
/// - If the message is so wrong that we didn't even create a database entry,
|
||||
/// returns `Ok(None)`
|
||||
/// If possible, creates a database entry to prevent the message from being
|
||||
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`.
|
||||
/// If the message is so wrong that we didn't even create a database entry,
|
||||
/// returns `Ok(None)`.
|
||||
///
|
||||
/// If `is_partial_download` is set, it contains the full message size in bytes.
|
||||
/// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later.
|
||||
@@ -107,7 +101,28 @@ pub(crate) async fn receive_imf_inner(
|
||||
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf: can't parse MIME: {}", err);
|
||||
return Ok(None);
|
||||
let msg_ids;
|
||||
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?;
|
||||
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
} else {
|
||||
return Ok(None);
|
||||
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
||||
}
|
||||
|
||||
return Ok(Some(ReceivedMsg {
|
||||
chat_id: DC_CHAT_ID_TRASH,
|
||||
state: MessageState::Undefined,
|
||||
sort_timestamp: 0,
|
||||
msg_ids,
|
||||
needs_delete_job: false,
|
||||
}));
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
};
|
||||
@@ -141,7 +156,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
None
|
||||
};
|
||||
|
||||
// the function returns the number of created messages in the database
|
||||
let prevent_rename =
|
||||
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
|
||||
|
||||
@@ -168,7 +182,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
prevent_rename,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -353,28 +366,25 @@ pub(crate) async fn receive_imf_inner(
|
||||
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
|
||||
pub async fn from_field_to_contact_id(
|
||||
context: &Context,
|
||||
from_address_list: &[SingleInfo],
|
||||
from: &SingleInfo,
|
||||
prevent_rename: bool,
|
||||
) -> Result<(ContactId, bool, Origin)> {
|
||||
let from_ids = add_or_lookup_contacts_by_address_list(
|
||||
let display_name = if prevent_rename {
|
||||
Some("")
|
||||
} else {
|
||||
from.display_name.as_deref()
|
||||
};
|
||||
let from_id = add_or_lookup_contact_by_addr(
|
||||
context,
|
||||
from_address_list,
|
||||
display_name,
|
||||
&from.addr,
|
||||
Origin::IncomingUnknownFrom,
|
||||
prevent_rename,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if from_ids.contains(&ContactId::SELF) {
|
||||
if from_id == ContactId::SELF {
|
||||
Ok((ContactId::SELF, false, Origin::OutgoingBcc))
|
||||
} else if !from_ids.is_empty() {
|
||||
if from_ids.len() > 1 {
|
||||
warn!(
|
||||
context,
|
||||
"mail has more than one From address, only using first: {:?}", from_address_list
|
||||
);
|
||||
}
|
||||
let from_id = from_ids.get(0).cloned().unwrap_or_default();
|
||||
|
||||
} else {
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
if let Ok(contact) = Contact::load_from_db(context, from_id).await {
|
||||
@@ -382,13 +392,6 @@ pub async fn from_field_to_contact_id(
|
||||
incoming_origin = contact.origin;
|
||||
}
|
||||
Ok((from_id, from_id_blocked, incoming_origin))
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"mail has an empty From header: {:?}", from_address_list
|
||||
);
|
||||
|
||||
Ok((ContactId::UNDEFINED, false, Origin::Unknown))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,9 +573,10 @@ async fn add_parts(
|
||||
if chat.is_protected() {
|
||||
let s = stock_str::unknown_sender_for_chat(context).await;
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
} else if let Some(from) = mime_parser.from.first() {
|
||||
} else {
|
||||
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
|
||||
// to the sender's name, indicating to the user that he/she is not part of the group.
|
||||
let from = &mime_parser.from;
|
||||
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
@@ -637,11 +641,9 @@ async fn add_parts(
|
||||
// if contact renaming is prevented (for mailinglists and bots),
|
||||
// we use name from From:-header as override name
|
||||
if prevent_rename {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if let Some(name) = &from.display_name {
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
if let Some(name) = &mime_parser.from.display_name {
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1508,7 +1510,10 @@ async fn create_or_lookup_group(
|
||||
|
||||
let grpname = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.context("Chat-Group-Name vanished")?;
|
||||
.context("Chat-Group-Name vanished")?
|
||||
// W/a for "Space added before long group names after MIME serialization/deserialization
|
||||
// #3650" issue. DC itself never creates group names with leading/trailing whitespace.
|
||||
.trim();
|
||||
let new_chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
@@ -1525,19 +1530,13 @@ async fn create_or_lookup_group(
|
||||
chat_id_blocked = create_blocked;
|
||||
|
||||
// Create initial member list.
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, ContactId::SELF).await?;
|
||||
if !from_id.is_special() && !chat::is_contact_in_chat(context, new_chat_id, from_id).await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, from_id).await?;
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
|
||||
if to_id != ContactId::SELF
|
||||
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
|
||||
}
|
||||
let mut members = vec![ContactId::SELF];
|
||||
if !from_id.is_special() {
|
||||
members.push(from_id);
|
||||
}
|
||||
members.extend(to_ids);
|
||||
members.dedup();
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
|
||||
|
||||
// once, we have protected-chats explained in UI, we can uncomment the following lines.
|
||||
// ("verified groups" did not add a message anyway)
|
||||
@@ -1616,9 +1615,15 @@ async fn apply_group_changes(
|
||||
{
|
||||
better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await);
|
||||
recreate_member_list = true;
|
||||
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|s| s.trim())
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
if chat_id
|
||||
@@ -1687,6 +1692,7 @@ async fn apply_group_changes(
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
let mut members_to_add = vec![];
|
||||
if removed_id.is_some()
|
||||
|| !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
{
|
||||
@@ -1701,26 +1707,23 @@ async fn apply_group_changes(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if removed_id != Some(ContactId::SELF) {
|
||||
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
|
||||
}
|
||||
members_to_add.push(ContactId::SELF);
|
||||
}
|
||||
if !from_id.is_special()
|
||||
&& from_id != ContactId::SELF
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id).await?
|
||||
&& removed_id != Some(from_id)
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, from_id).await?;
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.push(from_id);
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
if to_id != ContactId::SELF
|
||||
&& !chat::is_contact_in_chat(context, chat_id, to_id).await?
|
||||
&& removed_id != Some(to_id)
|
||||
{
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
|
||||
chat::add_to_chat_contacts_table(context, chat_id, to_id).await?;
|
||||
}
|
||||
members_to_add.extend(to_ids);
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.retain(|id| *id != removed_id);
|
||||
}
|
||||
members_to_add.dedup();
|
||||
|
||||
info!(
|
||||
context,
|
||||
"adding {:?} to chat id={}", members_to_add, chat_id
|
||||
);
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &members_to_add).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
@@ -1801,10 +1804,8 @@ async fn create_or_lookup_mailinglist(
|
||||
// a usable name for these lists is in the `From` header
|
||||
// and we can detect these lists by a unique `ListId`-suffix.
|
||||
if listid.ends_with(".list-id.mcsv.net") {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if let Some(display_name) = &from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1824,18 +1825,15 @@ async fn create_or_lookup_mailinglist(
|
||||
//
|
||||
// this pattern is similar to mailchimp above, however,
|
||||
// with weaker conditions and does not overwrite existing names.
|
||||
if name.is_empty() {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if from.addr.contains("noreply")
|
||||
|| from.addr.contains("no-reply")
|
||||
|| from.addr.starts_with("notifications@")
|
||||
|| from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local")
|
||||
{
|
||||
if let Some(display_name) = &from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
}
|
||||
if name.is_empty()
|
||||
&& (mime_parser.from.addr.contains("noreply")
|
||||
|| mime_parser.from.addr.contains("no-reply")
|
||||
|| mime_parser.from.addr.starts_with("notifications@")
|
||||
|| mime_parser.from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local"))
|
||||
{
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1860,12 +1858,18 @@ async fn create_or_lookup_mailinglist(
|
||||
p.to_string()
|
||||
});
|
||||
|
||||
let is_bot = context.get_config(Config::Bot).await?.is_some();
|
||||
let blocked = if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
};
|
||||
let chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Mailinglist,
|
||||
&listid,
|
||||
&name,
|
||||
Blocked::Request,
|
||||
blocked,
|
||||
ProtectionStatus::Unprotected,
|
||||
param,
|
||||
)
|
||||
@@ -1877,8 +1881,8 @@ async fn create_or_lookup_mailinglist(
|
||||
)
|
||||
})?;
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
|
||||
Ok(Some((chat_id, Blocked::Request)))
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
|
||||
Ok(Some((chat_id, blocked)))
|
||||
} else {
|
||||
info!(context, "creating list forbidden by caller");
|
||||
Ok(None)
|
||||
@@ -2007,9 +2011,7 @@ async fn create_adhoc_group(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
for &member_id in member_ids.iter() {
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, member_id).await?;
|
||||
}
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
|
||||
@@ -2123,7 +2125,7 @@ async fn check_verified_properties(
|
||||
&fp,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
is_verified = true;
|
||||
}
|
||||
}
|
||||
@@ -2233,7 +2235,6 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
origin: Origin,
|
||||
prevent_rename: bool,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let mut contact_ids = HashSet::new();
|
||||
for info in address_list.iter() {
|
||||
@@ -2241,11 +2242,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
if !may_be_valid_addr(addr) {
|
||||
continue;
|
||||
}
|
||||
let display_name = if prevent_rename {
|
||||
Some("")
|
||||
} else {
|
||||
info.display_name.as_deref()
|
||||
};
|
||||
let display_name = info.display_name.as_deref();
|
||||
contact_ids
|
||||
.insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?);
|
||||
}
|
||||
@@ -2276,6 +2273,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::get_chat_contacts;
|
||||
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
@@ -2288,7 +2286,7 @@ mod tests {
|
||||
async fn test_grpid_simple() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello\n\
|
||||
From: hello@example.org\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
@@ -2303,11 +2301,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_grpid_from_multiple() {
|
||||
async fn test_bad_from() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
\n\
|
||||
hello\x00";
|
||||
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
|
||||
assert!(mimeparser.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_grpid_from_multiple() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello@example.org\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
|
||||
References: <qweqweqwe>, <Gr.HcxyMARjyJy.9-uvzWPTLtV@nau.ca>\n\
|
||||
\n\
|
||||
@@ -2593,8 +2605,55 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
// Check that the message was added to the database:
|
||||
assert!(chats.get_msg_id(0).is_ok());
|
||||
// Check that the message is not shown to the user:
|
||||
assert!(chats.is_empty());
|
||||
|
||||
// Check that the message was added to the db:
|
||||
assert!(message::rfc724_mid_exists(context, "3924@example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// If there is no Message-Id header, we generate a random id.
|
||||
/// But there is no point in adding a trash entry in the database
|
||||
/// if the email is malformed (e.g. because `From` is missing)
|
||||
/// with this random id we just generated.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_message_id_header() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert!(chats.get_msg_id(0).is_err());
|
||||
|
||||
let received = receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
To: bob@example.com\n\
|
||||
Subject: foo\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
dbg!(&received);
|
||||
assert!(received.is_none());
|
||||
|
||||
assert!(!t
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
// Check that the message is not shown to the user:
|
||||
assert!(chats.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -3633,6 +3692,27 @@ Hello mailinglist!\r\n"
|
||||
assert_eq!(chat.name, "Test1");
|
||||
}
|
||||
|
||||
/// Tests that bots automatically accept mailing lists.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mailing_list_bot() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/mailinglist_chat_message.eml"),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
|
||||
// Bot should see the message as fresh and process it.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_show_tokens_in_contacts_list() {
|
||||
check_dont_show_in_contacts_list(
|
||||
@@ -5097,13 +5177,10 @@ Reply from different address
|
||||
chat::add_to_chat_contacts_table(
|
||||
&bob,
|
||||
group_id,
|
||||
bob.add_or_lookup_contact(&alice1).await.id,
|
||||
)
|
||||
.await?;
|
||||
chat::add_to_chat_contacts_table(
|
||||
&bob,
|
||||
group_id,
|
||||
Contact::create(&bob, "", "charlie@example.org").await?,
|
||||
&[
|
||||
bob.add_or_lookup_contact(&alice1).await.id,
|
||||
Contact::create(&bob, "", "charlie@example.org").await?,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -5184,7 +5261,7 @@ Reply from different address
|
||||
chat::add_to_chat_contacts_table(
|
||||
&bob,
|
||||
group_id,
|
||||
bob.add_or_lookup_contact(&alice).await.id,
|
||||
&[bob.add_or_lookup_contact(&alice).await.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -5237,4 +5314,20 @@ Reply from different address
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,12 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Implement a single iteration of IMAP loop.
|
||||
///
|
||||
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
|
||||
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
|
||||
/// critical operation fails such as fetching new messages fails, connection is reset via
|
||||
/// `trigger_reconnect`, so a fresh one can be opened.
|
||||
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) -> InterruptInfo {
|
||||
let folder = match ctx.get_config(folder_config).await {
|
||||
Ok(folder) => folder,
|
||||
@@ -187,18 +193,25 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
};
|
||||
|
||||
// connect and fake idle if unable to connect
|
||||
if let Err(err) = connection.prepare(ctx).await {
|
||||
warn!(ctx, "imap connection failed: {}", err);
|
||||
if let Err(err) = connection
|
||||
.prepare(ctx)
|
||||
.await
|
||||
.context("prepare IMAP connection")
|
||||
{
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
if folder_config == Config::ConfiguredInboxFolder {
|
||||
if let Err(err) = connection
|
||||
.store_seen_flags_on_imap(ctx)
|
||||
.await
|
||||
.context("store_seen_flags_on_imap failed")
|
||||
{
|
||||
warn!(ctx, "{:#}", err);
|
||||
if let Some(session) = connection.session.as_mut() {
|
||||
session
|
||||
.store_seen_flags_on_imap(ctx)
|
||||
.await
|
||||
.context("store_seen_flags_on_imap")
|
||||
.ok_or_log(ctx);
|
||||
} else {
|
||||
warn!(ctx, "No session even though we just prepared it");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +221,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
.await
|
||||
.context("fetch_move_delete")
|
||||
{
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
@@ -217,12 +230,10 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
if let Err(err) = delete_expired_imap_messages(ctx)
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")
|
||||
{
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
.ok_or_log(ctx);
|
||||
|
||||
// Scan additional folders only after finishing fetching the watched folder.
|
||||
//
|
||||
@@ -248,7 +259,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
.await
|
||||
.context("fetch_move_delete after scan_folders")
|
||||
{
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
@@ -266,18 +277,38 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
|
||||
connection.connectivity.set_connected(ctx).await;
|
||||
|
||||
// idle
|
||||
if !connection.can_idle() {
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
match connection.idle(ctx, Some(watch_folder)).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
InterruptInfo::new(false)
|
||||
if let Some(session) = connection.session.take() {
|
||||
if !session.can_idle() {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
info!(ctx, "IMAP session supports IDLE, using it.");
|
||||
match session
|
||||
.idle(
|
||||
ctx,
|
||||
connection.idle_interrupt_receiver.clone(),
|
||||
Some(watch_folder),
|
||||
)
|
||||
.await
|
||||
.context("idle")
|
||||
{
|
||||
Ok((session, info)) => {
|
||||
connection.session = Some(session);
|
||||
info
|
||||
}
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
InterruptInfo::new(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(ctx, "No IMAP session, going to fake idle.");
|
||||
connection.fake_idle(ctx, Some(watch_folder)).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::tools::time;
|
||||
use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
|
||||
use crate::{context::Context, log::LogExt};
|
||||
use anyhow::{anyhow, Result};
|
||||
use humansize::{file_size_opts, FileSize};
|
||||
use humansize::{format_size, BINARY};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
|
||||
pub enum Connectivity {
|
||||
@@ -448,7 +448,7 @@ impl Context {
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
|
||||
let domain = &tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
|
||||
let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
|
||||
ret += &format!("<h3>{}</h3><ul>", storage_on_domain);
|
||||
let quota = self.quota.read().await;
|
||||
@@ -473,8 +473,8 @@ impl Context {
|
||||
let messages = stock_str::messages(self).await;
|
||||
let part_of_total_used = stock_str::part_of_total_used(
|
||||
self,
|
||||
resource.usage.to_string(),
|
||||
resource.limit.to_string(),
|
||||
&resource.usage.to_string(),
|
||||
&resource.limit.to_string(),
|
||||
)
|
||||
.await;
|
||||
ret += &match &resource.name {
|
||||
@@ -495,12 +495,8 @@ impl Context {
|
||||
// - the string is not longer than the other strings that way (minus title, plus units) -
|
||||
// additional linebreaks on small displays are unlikely therefore
|
||||
// - most times, this is the only item anyway
|
||||
let usage = (resource.usage * 1024)
|
||||
.file_size(file_size_opts::BINARY)
|
||||
.unwrap_or_default();
|
||||
let limit = (resource.limit * 1024)
|
||||
.file_size(file_size_opts::BINARY)
|
||||
.unwrap_or_default();
|
||||
let usage = &format_size(resource.usage * 1024, BINARY);
|
||||
let limit = &format_size(resource.limit * 1024, BINARY);
|
||||
stock_str::part_of_total_used(self, usage, limit).await
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::qr::check_qr;
|
||||
use crate::stock_str;
|
||||
use crate::token;
|
||||
@@ -640,11 +640,7 @@ async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) ->
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
) {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
peerstate
|
||||
.save_to_db(&context.sql, false)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -932,10 +928,9 @@ mod tests {
|
||||
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql, true).await?;
|
||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// TODO: how does this group become usable?
|
||||
let group_chat_id = state.joining_chat_id(context).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id())
|
||||
chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
|
||||
.await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
|
||||
42
src/sql.rs
42
src/sql.rs
@@ -280,7 +280,7 @@ impl Sql {
|
||||
for addr in &addrs {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(self, false).await?;
|
||||
peerstate.save_to_db(self).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,7 +432,7 @@ impl Sql {
|
||||
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
|
||||
where
|
||||
H: Send + 'static,
|
||||
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
{
|
||||
let mut conn = self.get_conn().await?;
|
||||
tokio::task::block_in_place(move || {
|
||||
@@ -528,9 +528,7 @@ impl Sql {
|
||||
///
|
||||
/// Setting `None` deletes the value. On failure an error message
|
||||
/// will already have been logged.
|
||||
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
|
||||
let key = key.as_ref();
|
||||
|
||||
pub async fn set_raw_config(&self, key: &str, value: Option<&str>) -> Result<()> {
|
||||
let mut lock = self.config_cache.write().await;
|
||||
if let Some(value) = value {
|
||||
let exists = self
|
||||
@@ -564,9 +562,9 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Get configuration options from the database.
|
||||
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
|
||||
pub async fn get_raw_config(&self, key: &str) -> Result<Option<String>> {
|
||||
let lock = self.config_cache.read().await;
|
||||
let cached = lock.get(key.as_ref()).cloned();
|
||||
let cached = lock.get(key).cloned();
|
||||
drop(lock);
|
||||
|
||||
if let Some(c) = cached {
|
||||
@@ -575,48 +573,42 @@ impl Sql {
|
||||
|
||||
let mut lock = self.config_cache.write().await;
|
||||
let value = self
|
||||
.query_get_value(
|
||||
"SELECT value FROM config WHERE keyname=?;",
|
||||
paramsv![key.as_ref()],
|
||||
)
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv![key])
|
||||
.await
|
||||
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
|
||||
lock.insert(key.as_ref().to_string(), value.clone());
|
||||
.context(format!("failed to fetch raw config: {}", key))?;
|
||||
lock.insert(key.to_string(), value.clone());
|
||||
drop(lock);
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> {
|
||||
pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> {
|
||||
self.set_raw_config(key, Some(&format!("{}", value))).await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Result<Option<i32>> {
|
||||
pub async fn get_raw_config_int(&self, key: &str) -> Result<Option<i32>> {
|
||||
self.get_raw_config(key)
|
||||
.await
|
||||
.map(|s| s.and_then(|s| s.parse().ok()))
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> Result<bool> {
|
||||
pub async fn get_raw_config_bool(&self, key: &str) -> Result<bool> {
|
||||
// Not the most obvious way to encode bool as string, but it is matter
|
||||
// of backward compatibility.
|
||||
let res = self.get_raw_config_int(key).await?;
|
||||
Ok(res.unwrap_or_default() > 0)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_bool<T>(&self, key: T, value: bool) -> Result<()>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> {
|
||||
let value = if value { Some("1") } else { None };
|
||||
self.set_raw_config(key, value).await
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int64(&self, key: impl AsRef<str>, value: i64) -> Result<()> {
|
||||
pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> {
|
||||
self.set_raw_config(key, Some(&format!("{}", value))).await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Result<Option<i64>> {
|
||||
pub async fn get_raw_config_int64(&self, key: &str) -> Result<Option<i64>> {
|
||||
self.get_raw_config(key)
|
||||
.await
|
||||
.map(|s| s.and_then(|r| r.parse().ok()))
|
||||
@@ -728,7 +720,7 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
for row in rows {
|
||||
maybe_add_file(&mut files_in_use, row?);
|
||||
maybe_add_file(&mut files_in_use, &row?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -819,8 +811,8 @@ fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, nam
|
||||
files_in_use.contains(name_to_check)
|
||||
}
|
||||
|
||||
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
|
||||
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") {
|
||||
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: &str) {
|
||||
if let Some(file) = file.strip_prefix("$BLOBDIR/") {
|
||||
files_in_use.insert(file.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +320,11 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
|
||||
if dbversion < 67 {
|
||||
for prefix in &["", "configured_"] {
|
||||
if let Some(server_flags) = sql
|
||||
.get_raw_config_int(format!("{}server_flags", prefix))
|
||||
.get_raw_config_int(&format!("{}server_flags", prefix))
|
||||
.await?
|
||||
{
|
||||
let imap_socket_flags = server_flags & 0x700;
|
||||
let key = format!("{}mail_security", prefix);
|
||||
let key = &format!("{}mail_security", prefix);
|
||||
match imap_socket_flags {
|
||||
0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
|
||||
0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
|
||||
@@ -332,7 +332,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
|
||||
_ => sql.set_raw_config_int(key, 0).await?,
|
||||
}
|
||||
let smtp_socket_flags = server_flags & 0x70000;
|
||||
let key = format!("{}send_security", prefix);
|
||||
let key = &format!("{}send_security", prefix);
|
||||
match smtp_socket_flags {
|
||||
0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
|
||||
0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
|
||||
@@ -616,6 +616,50 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 94 {
|
||||
sql.execute_migration(
|
||||
// Create new `acpeerstates` table, same as before but with unique constraint.
|
||||
//
|
||||
// This allows to use `UPSERT` to update existing or insert a new peerstate
|
||||
// depending on whether one exists already.
|
||||
"CREATE TABLE new_acpeerstates (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
last_seen_autocrypt INTEGER DEFAULT 0,
|
||||
public_key,
|
||||
prefer_encrypted INTEGER DEFAULT 0,
|
||||
gossip_timestamp INTEGER DEFAULT 0,
|
||||
gossip_key,
|
||||
public_key_fingerprint TEXT DEFAULT '',
|
||||
gossip_key_fingerprint TEXT DEFAULT '',
|
||||
verified_key,
|
||||
verified_key_fingerprint TEXT DEFAULT '',
|
||||
UNIQUE (addr) -- Only one peerstate per address
|
||||
);
|
||||
INSERT OR IGNORE INTO new_acpeerstates SELECT * FROM acpeerstates;
|
||||
DROP TABLE acpeerstates;
|
||||
ALTER TABLE new_acpeerstates RENAME TO acpeerstates;
|
||||
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
|
||||
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
|
||||
",
|
||||
94,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 95 {
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\
|
||||
INSERT OR IGNORE INTO new_chats_contacts SELECT * FROM chats_contacts;\
|
||||
DROP TABLE chats_contacts;\
|
||||
ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\
|
||||
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);",
|
||||
95
|
||||
).await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
|
||||
148
src/stock_str.rs
148
src/stock_str.rs
@@ -17,7 +17,7 @@ use crate::context::Context;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::tools::timestamp_to_str;
|
||||
use humansize::{file_size_opts, FileSize};
|
||||
use humansize::{format_size, BINARY};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StockStrings {
|
||||
@@ -466,33 +466,33 @@ async fn translated(context: &Context, id: StockMessage) -> String {
|
||||
/// Helper trait only meant to be implemented for [`String`].
|
||||
trait StockStringMods: AsRef<str> + Sized {
|
||||
/// Substitutes the first replacement value if one is present.
|
||||
fn replace1(&self, replacement: impl AsRef<str>) -> String {
|
||||
fn replace1(&self, replacement: &str) -> String {
|
||||
self.as_ref()
|
||||
.replacen("%1$s", replacement.as_ref(), 1)
|
||||
.replacen("%1$d", replacement.as_ref(), 1)
|
||||
.replacen("%1$@", replacement.as_ref(), 1)
|
||||
.replacen("%1$s", replacement, 1)
|
||||
.replacen("%1$d", replacement, 1)
|
||||
.replacen("%1$@", replacement, 1)
|
||||
}
|
||||
|
||||
/// Substitutes the second replacement value if one is present.
|
||||
///
|
||||
/// Be aware you probably should have also called [`StockStringMods::replace1`] if
|
||||
/// you are calling this.
|
||||
fn replace2(&self, replacement: impl AsRef<str>) -> String {
|
||||
fn replace2(&self, replacement: &str) -> String {
|
||||
self.as_ref()
|
||||
.replacen("%2$s", replacement.as_ref(), 1)
|
||||
.replacen("%2$d", replacement.as_ref(), 1)
|
||||
.replacen("%2$@", replacement.as_ref(), 1)
|
||||
.replacen("%2$s", replacement, 1)
|
||||
.replacen("%2$d", replacement, 1)
|
||||
.replacen("%2$@", replacement, 1)
|
||||
}
|
||||
|
||||
/// Substitutes the third replacement value if one is present.
|
||||
///
|
||||
/// Be aware you probably should have also called [`StockStringMods::replace1`] and
|
||||
/// [`StockStringMods::replace2`] if you are calling this.
|
||||
fn replace3(&self, replacement: impl AsRef<str>) -> String {
|
||||
fn replace3(&self, replacement: &str) -> String {
|
||||
self.as_ref()
|
||||
.replacen("%3$s", replacement.as_ref(), 1)
|
||||
.replacen("%3$d", replacement.as_ref(), 1)
|
||||
.replacen("%3$@", replacement.as_ref(), 1)
|
||||
.replacen("%3$s", replacement, 1)
|
||||
.replacen("%3$d", replacement, 1)
|
||||
.replacen("%3$@", replacement, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,8 +551,8 @@ pub(crate) async fn file(context: &Context) -> String {
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
pub(crate) async fn msg_grp_name(
|
||||
context: &Context,
|
||||
from_group: impl AsRef<str>,
|
||||
to_group: impl AsRef<str>,
|
||||
from_group: &str,
|
||||
to_group: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -565,7 +565,7 @@ pub(crate) async fn msg_grp_name(
|
||||
.await
|
||||
.replace1(from_group)
|
||||
.replace2(to_group)
|
||||
.replace3(by_contact.get_stock_name(context).await)
|
||||
.replace3(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +575,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGrpImgChangedBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,11 +585,11 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
||||
/// contacts to combine with the display name.
|
||||
pub(crate) async fn msg_add_member(
|
||||
context: &Context,
|
||||
added_member_addr: impl AsRef<str>,
|
||||
added_member_addr: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = added_member_addr.as_ref();
|
||||
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
let addr = added_member_addr;
|
||||
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
@@ -604,7 +604,7 @@ pub(crate) async fn msg_add_member(
|
||||
translated(context, StockMessage::MsgAddMemberBy)
|
||||
.await
|
||||
.replace1(who)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,11 +614,11 @@ pub(crate) async fn msg_add_member(
|
||||
/// the contacts to combine with the display name.
|
||||
pub(crate) async fn msg_del_member(
|
||||
context: &Context,
|
||||
removed_member_addr: impl AsRef<str>,
|
||||
removed_member_addr: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = removed_member_addr.as_ref();
|
||||
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
let addr = removed_member_addr;
|
||||
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
@@ -633,7 +633,7 @@ pub(crate) async fn msg_del_member(
|
||||
translated(context, StockMessage::MsgDelMemberBy)
|
||||
.await
|
||||
.replace1(who)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) ->
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGroupLeftBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ pub(crate) async fn read_rcpt(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `This is a return receipt for the message "%1$s".`.
|
||||
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: &str) -> String {
|
||||
translated(context, StockMessage::ReadRcptMailBody)
|
||||
.await
|
||||
.replace1(message)
|
||||
@@ -697,7 +697,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGrpImgDeletedBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ pub(crate) async fn secure_join_started(
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinStarted)
|
||||
.await
|
||||
.replace1(contact.get_name_n_addr())
|
||||
.replace1(&contact.get_name_n_addr())
|
||||
.replace2(contact.get_display_name())
|
||||
} else {
|
||||
format!(
|
||||
@@ -741,7 +741,7 @@ pub(crate) async fn setup_contact_qr_description(
|
||||
display_name: &str,
|
||||
addr: &str,
|
||||
) -> String {
|
||||
let name = if display_name == addr {
|
||||
let name = &if display_name == addr {
|
||||
addr.to_owned()
|
||||
} else {
|
||||
format!("{} ({})", display_name, addr)
|
||||
@@ -760,7 +760,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_name_n_addr();
|
||||
let addr = &contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
.await
|
||||
.replace1(addr)
|
||||
@@ -768,17 +768,14 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
|
||||
|
||||
/// Stock string: `Cannot verify %1$s`.
|
||||
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_name_n_addr();
|
||||
let addr = &contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactNotVerified)
|
||||
.await
|
||||
.replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Changed setup for %1$s`.
|
||||
pub(crate) async fn contact_setup_changed(
|
||||
context: &Context,
|
||||
contact_addr: impl AsRef<str>,
|
||||
) -> String {
|
||||
pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
|
||||
translated(context, StockMessage::ContactSetupChanged)
|
||||
.await
|
||||
.replace1(contact_addr)
|
||||
@@ -810,7 +807,7 @@ pub(crate) async fn sync_msg_body(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
|
||||
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
|
||||
translated(context, StockMessage::CannotLogin)
|
||||
.await
|
||||
.replace1(user)
|
||||
@@ -828,7 +825,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
|
||||
} else {
|
||||
translated(context, StockMessage::MsgLocationEnabledBy)
|
||||
.await
|
||||
.replace1(contact.get_stock_name(context).await)
|
||||
.replace1(&contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,17 +871,14 @@ pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
|
||||
|
||||
/// Stock string: `Message from %1$s`.
|
||||
// TODO: This can compute `self_name` itself instead of asking the caller to do this.
|
||||
pub(crate) async fn subject_for_new_contact(
|
||||
context: &Context,
|
||||
self_name: impl AsRef<str>,
|
||||
) -> String {
|
||||
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
|
||||
translated(context, StockMessage::SubjectForNewContact)
|
||||
.await
|
||||
.replace1(self_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Failed to send message to %1$s.`.
|
||||
pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
|
||||
translated(context, StockMessage::FailedSendingTo)
|
||||
.await
|
||||
.replace1(name)
|
||||
@@ -900,14 +894,14 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s s.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
context: &Context,
|
||||
timer: impl AsRef<str>,
|
||||
timer: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -918,7 +912,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
|
||||
.await
|
||||
.replace1(timer)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,7 +923,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -940,7 +934,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerHourBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,7 +945,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDayBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,7 +956,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,14 +966,14 @@ pub(crate) async fn videochat_invitation(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `You are invited to a video chat, click %1$s to join.`.
|
||||
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
|
||||
translated(context, StockMessage::VideochatInviteMsgBody)
|
||||
.await
|
||||
.replace1(url)
|
||||
}
|
||||
|
||||
/// Stock string: `Error:\n\n“%1$s”`.
|
||||
pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
||||
translated(context, StockMessage::ConfigurationFailed)
|
||||
.await
|
||||
.replace1(details)
|
||||
@@ -987,7 +981,7 @@ pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef<
|
||||
|
||||
/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
|
||||
// TODO: This could compute now itself.
|
||||
pub(crate) async fn bad_time_msg_body(context: &Context, now: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
|
||||
translated(context, StockMessage::BadTimeMsgBody)
|
||||
.await
|
||||
.replace1(now)
|
||||
@@ -1010,7 +1004,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId)
|
||||
} else {
|
||||
translated(context, StockMessage::ProtectionEnabledBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,7 +1015,7 @@ pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId
|
||||
} else {
|
||||
translated(context, StockMessage::ProtectionDisabledBy)
|
||||
.await
|
||||
.replace1(by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,7 +1037,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
|
||||
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
context: &Context,
|
||||
minutes: impl AsRef<str>,
|
||||
minutes: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1054,14 +1048,14 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
|
||||
.await
|
||||
.replace1(minutes)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s hours.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
context: &Context,
|
||||
hours: impl AsRef<str>,
|
||||
hours: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1072,14 +1066,14 @@ pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
|
||||
.await
|
||||
.replace1(hours)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s days.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_days(
|
||||
context: &Context,
|
||||
days: impl AsRef<str>,
|
||||
days: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1090,14 +1084,14 @@ pub(crate) async fn msg_ephemeral_timer_days(
|
||||
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
|
||||
.await
|
||||
.replace1(days)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s weeks.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_weeks(
|
||||
context: &Context,
|
||||
weeks: impl AsRef<str>,
|
||||
weeks: &str,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
@@ -1108,7 +1102,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
|
||||
.await
|
||||
.replace1(weeks)
|
||||
.replace2(by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,15 +1115,13 @@ pub(crate) async fn forwarded(context: &Context) -> String {
|
||||
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||
translated(context, StockMessage::QuotaExceedingMsgBody)
|
||||
.await
|
||||
.replace1(format!("{}", highest_usage))
|
||||
.replace1(&format!("{}", highest_usage))
|
||||
.replace("%%", "%")
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
|
||||
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
|
||||
let size = org_bytes
|
||||
.file_size(file_size_opts::BINARY)
|
||||
.unwrap_or_default();
|
||||
let size = &format_size(org_bytes, BINARY);
|
||||
translated(context, StockMessage::PartialDownloadMsgBody)
|
||||
.await
|
||||
.replace1(size)
|
||||
@@ -1139,7 +1131,7 @@ pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32)
|
||||
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
|
||||
translated(context, StockMessage::DownloadAvailability)
|
||||
.await
|
||||
.replace1(timestamp_to_str(timestamp))
|
||||
.replace1(×tamp_to_str(timestamp))
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming Messages`.
|
||||
@@ -1154,7 +1146,7 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
|
||||
|
||||
/// Stock string: `Storage on %1$s`.
|
||||
/// `%1$s` will be replaced by the domain of the configured email-address.
|
||||
pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
|
||||
translated(context, StockMessage::StorageOnDomain)
|
||||
.await
|
||||
.replace1(domain)
|
||||
@@ -1192,7 +1184,7 @@ pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
|
||||
|
||||
/// Stock string: `Error: %1$s…`.
|
||||
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
|
||||
pub(crate) async fn error(context: &Context, error: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn error(context: &Context, error: &str) -> String {
|
||||
translated(context, StockMessage::Error)
|
||||
.await
|
||||
.replace1(error)
|
||||
@@ -1210,11 +1202,7 @@ pub(crate) async fn messages(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s of %2$s used`.
|
||||
pub(crate) async fn part_of_total_used(
|
||||
context: &Context,
|
||||
part: impl AsRef<str>,
|
||||
total: impl AsRef<str>,
|
||||
) -> String {
|
||||
pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
|
||||
translated(context, StockMessage::PartOfTotallUsed)
|
||||
.await
|
||||
.replace1(part)
|
||||
@@ -1230,9 +1218,9 @@ pub(crate) async fn broadcast_list(context: &Context) -> String {
|
||||
/// Stock string: `%1$s changed their address from %2$s to %3$s`.
|
||||
pub(crate) async fn aeap_addr_changed(
|
||||
context: &Context,
|
||||
contact_name: impl AsRef<str>,
|
||||
old_addr: impl AsRef<str>,
|
||||
new_addr: impl AsRef<str>,
|
||||
contact_name: &str,
|
||||
old_addr: &str,
|
||||
new_addr: &str,
|
||||
) -> String {
|
||||
translated(context, StockMessage::AeapAddrChanged)
|
||||
.await
|
||||
@@ -1243,8 +1231,8 @@ pub(crate) async fn aeap_addr_changed(
|
||||
|
||||
pub(crate) async fn aeap_explanation_and_link(
|
||||
context: &Context,
|
||||
old_addr: impl AsRef<str>,
|
||||
new_addr: impl AsRef<str>,
|
||||
old_addr: &str,
|
||||
new_addr: &str,
|
||||
) -> String {
|
||||
translated(context, StockMessage::AeapExplanationAndLink)
|
||||
.await
|
||||
|
||||
@@ -380,13 +380,13 @@ impl TestContext {
|
||||
/// table. Messages are returned in the order they have been sent.
|
||||
///
|
||||
/// Panics if there is no message or on any error.
|
||||
pub async fn pop_sent_msg(&self) -> SentMessage {
|
||||
pub async fn pop_sent_msg(&self) -> SentMessage<'_> {
|
||||
self.pop_sent_msg_opt(Duration::from_secs(3))
|
||||
.await
|
||||
.expect("no sent message found in jobs table")
|
||||
}
|
||||
|
||||
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage> {
|
||||
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
|
||||
let start = Instant::now();
|
||||
let (rowid, msg_id, payload, recipients) = loop {
|
||||
let row = self
|
||||
@@ -428,6 +428,7 @@ impl TestContext {
|
||||
Some(SentMessage {
|
||||
payload,
|
||||
sender_msg_id: msg_id,
|
||||
sender_context: &self.ctx,
|
||||
recipients,
|
||||
})
|
||||
}
|
||||
@@ -439,7 +440,7 @@ impl TestContext {
|
||||
/// peerstates will be updated. Later receiving the message using [recv_msg] is
|
||||
/// unlikely to be affected as the peerstate would be processed again in exactly the
|
||||
/// same way.
|
||||
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
|
||||
pub async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
|
||||
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -447,7 +448,7 @@ impl TestContext {
|
||||
|
||||
/// Receive a message using the `receive_imf()` pipeline. Panics if it's not shown
|
||||
/// in the chat as exactly one message.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) -> Message {
|
||||
pub async fn recv_msg(&self, msg: &SentMessage<'_>) -> Message {
|
||||
let received = self
|
||||
.recv_msg_opt(msg)
|
||||
.await
|
||||
@@ -476,7 +477,10 @@ impl TestContext {
|
||||
|
||||
/// Receive a message using the `receive_imf()` pipeline. This is similar
|
||||
/// to `recv_msg()`, but doesn't assume that the message is shown in the chat.
|
||||
pub async fn recv_msg_opt(&self, msg: &SentMessage) -> Option<crate::receive_imf::ReceivedMsg> {
|
||||
pub async fn recv_msg_opt(
|
||||
&self,
|
||||
msg: &SentMessage<'_>,
|
||||
) -> Option<crate::receive_imf::ReceivedMsg> {
|
||||
receive_imf(self, msg.payload().as_bytes(), false)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -583,7 +587,7 @@ impl TestContext {
|
||||
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
|
||||
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
|
||||
/// the message.
|
||||
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage {
|
||||
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage<'_> {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(txt.to_string()));
|
||||
self.send_msg(chat_id, &mut msg).await
|
||||
@@ -594,7 +598,7 @@ impl TestContext {
|
||||
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
|
||||
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
|
||||
/// the message.
|
||||
pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage {
|
||||
pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage<'_> {
|
||||
chat::prepare_msg(self, chat_id, msg).await.unwrap();
|
||||
let msg_id = chat::send_msg(self, chat_id, msg).await.unwrap();
|
||||
let res = self.pop_sent_msg().await;
|
||||
@@ -742,13 +746,14 @@ impl Drop for LogSink {
|
||||
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
||||
/// passed through a SMTP-IMAP pipeline.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SentMessage {
|
||||
pub struct SentMessage<'a> {
|
||||
pub payload: String,
|
||||
recipients: String,
|
||||
pub sender_msg_id: MsgId,
|
||||
sender_context: &'a Context,
|
||||
}
|
||||
|
||||
impl SentMessage {
|
||||
impl SentMessage<'_> {
|
||||
/// A recipient the message was destined for.
|
||||
///
|
||||
/// If there are multiple recipients this is just a random one, so is not very useful.
|
||||
@@ -765,6 +770,12 @@ impl SentMessage {
|
||||
pub fn payload(&self) -> &str {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
pub async fn load_from_db(&self) -> Message {
|
||||
Message::load_from_db(self.sender_context, self.sender_msg_id)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.org from disk.
|
||||
@@ -979,7 +990,7 @@ fn print_event(event: &Event) {
|
||||
/// Logs an individual message to stdout.
|
||||
///
|
||||
/// This includes a bunch of the message meta-data as well.
|
||||
async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
async fn log_msg(context: &Context, prefix: &str, msg: &Message) {
|
||||
let contact = match Contact::get_by_id(context, msg.get_from_id()).await {
|
||||
Ok(contact) => contact,
|
||||
Err(e) => {
|
||||
@@ -1001,7 +1012,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
|
||||
prefix.as_ref(),
|
||||
prefix,
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
|
||||
@@ -341,9 +341,8 @@ async fn mark_as_verified(this: &TestContext, other: &TestContext) {
|
||||
|
||||
peerstate.verified_key = peerstate.public_key.clone();
|
||||
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
|
||||
peerstate.to_save = Some(peerstate::ToSave::All);
|
||||
|
||||
peerstate.save_to_db(&this.sql, false).await.unwrap();
|
||||
peerstate.save_to_db(&this.sql).await.unwrap();
|
||||
}
|
||||
|
||||
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
|
||||
|
||||
18
src/tools.rs
18
src/tools.rs
@@ -206,7 +206,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
msg.text = Some(
|
||||
stock_str::bad_time_msg_body(
|
||||
context,
|
||||
Local
|
||||
&Local
|
||||
.timestamp(now, 0)
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
@@ -319,8 +319,8 @@ pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
|
||||
}
|
||||
|
||||
// the returned suffix is lower-case
|
||||
pub fn get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||
Path::new(path_filename.as_ref())
|
||||
pub fn get_filesuffix_lc(path_filename: &str) -> Option<String> {
|
||||
Path::new(path_filename)
|
||||
.extension()
|
||||
.map(|p| p.to_string_lossy().to_lowercase())
|
||||
}
|
||||
@@ -670,6 +670,18 @@ pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// If `collection` contains exactly one element, return this element.
|
||||
/// Otherwise, return None.
|
||||
pub(crate) fn single_value<T>(collection: impl IntoIterator<Item = T>) -> Option<T> {
|
||||
let mut iter = collection.into_iter();
|
||||
if let Some(value) = iter.next() {
|
||||
if iter.next().is_none() {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
@@ -360,7 +360,9 @@ impl Context {
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(self, instance.chat_id).await?;
|
||||
ensure!(chat.can_send(self).await?, "cannot send to {}", chat.id);
|
||||
if let Some(reason) = chat.why_cant_send(self).await? {
|
||||
bail!("cannot send to {}: {}", chat.id, reason);
|
||||
}
|
||||
|
||||
let send_now = !matches!(
|
||||
instance.state,
|
||||
@@ -1074,7 +1076,7 @@ mod tests {
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_msg(chat.id, &mut alice_instance).await;
|
||||
let alice_instance = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
|
||||
let alice_instance = sent1.load_from_db().await;
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
alice_instance.id,
|
||||
@@ -1376,7 +1378,7 @@ mod tests {
|
||||
alice.flush_status_updates().await?;
|
||||
expect_status_update_event(&alice, alice_instance.id).await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
let alice_update = Message::load_from_db(&alice, sent2.sender_msg_id).await?;
|
||||
let alice_update = sent2.load_from_db().await;
|
||||
assert!(alice_update.hidden);
|
||||
assert_eq!(alice_update.viewtype, Viewtype::Text);
|
||||
assert_eq!(alice_update.get_filename(), None);
|
||||
@@ -2195,7 +2197,7 @@ sth_for_the = "future""#
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
let update_msg = Message::load_from_db(&alice, sent2.sender_msg_id).await?;
|
||||
let update_msg = sent2.load_from_db().await;
|
||||
assert!(alice_instance.get_showpadlock());
|
||||
assert!(update_msg.get_showpadlock());
|
||||
|
||||
@@ -2215,7 +2217,7 @@ sth_for_the = "future""#
|
||||
.await?;
|
||||
bob.flush_status_updates().await?;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
let update_msg = Message::load_from_db(&bob, sent3.sender_msg_id).await?;
|
||||
let update_msg = sent3.load_from_db().await;
|
||||
assert!(!update_msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
|
||||
93
test-data/message/thunderbird_with_autocrypt.eml
Normal file
93
test-data/message/thunderbird_with_autocrypt.eml
Normal file
@@ -0,0 +1,93 @@
|
||||
From - Thu, 24 Nov 2022 19:06:16 GMT
|
||||
X-Mozilla-Status: 0001
|
||||
X-Mozilla-Status2: 00800000
|
||||
Message-ID: <0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org>
|
||||
Date: Thu, 24 Nov 2022 20:05:57 +0100
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.4.2
|
||||
From: Alice <alice@example.org>
|
||||
To: bob@example.net
|
||||
Content-Language: en-US
|
||||
Autocrypt: addr=alice@example.org; keydata=
|
||||
xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN
|
||||
GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp
|
||||
7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M
|
||||
CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr
|
||||
RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp
|
||||
01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM
|
||||
AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy
|
||||
VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------EOdOT2kJUL5hgCilmIhYyVZg"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------EOdOT2kJUL5hgCilmIhYyVZg
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------EOdOT2kJUL5hgCilmIhYyVZg
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wV4D5tq63hTeebASAQdA1dVUsUjGZCOIfCnYtVdmOvKs/BNovI3sG8w1IH4ymTMwAZzgwVbGS5KL
|
||||
+e1VTD5mUTeVSEYe1cd3VozH4KbNJa1tBlcO0nzGwCPpsTVDMoxIwcBMA+PY3JvEjuMiAQf/d2yj
|
||||
t0+GyaptwX26bgSqo6vj21W8mcWS5vXOi8wjGwRbPaKKjS4kq1xDOz04eHrE8HUPD8otcXoI8CLz
|
||||
etJpRbFs0XJP4Cozbsr72dgoWhozRg/iSpBndxWOddTl7Yqo8m/fyhU5uzKZ41m2T8mha6KkKWD8
|
||||
QecGdOgieYBucNBjHwWc71p9G6jTnzfy4S4GtGS2gwOSMxpwO7HxpKzsHI4POqFSQbxrl/YRwWSC
|
||||
f5WqyYcerasIiR/fnOIw8lnvCeQ5rB90eGEDR70YFGt0t4rFBjfGrSPUiWYaTaC1Zvpd+t5sy7zy
|
||||
FpsS2/aTkwP/UpGqmtFaD/brSouRf9hijNLI0QFTaVmSoI3BKzF8B4zwvtEbOLZjyDb+Va/fZJ3w
|
||||
nYd2Q/5PPPL+pE4pWKN+jl0TZNzAaqBgvggXomgUqQ7QiksUzym+yuFKrJX0RF2awdrgjQIxjnda
|
||||
Qp3UFphnFTyYUJpIU9iewjOfVxgPzv7PyuCHYwoP3kh7MJZ6bgbDmOkeFSnjEDJpdf1m9xC9LlBL
|
||||
beC8scmPs6kx9GARBYSHvyPQ025gN3+XEHh4OrTxHZ91U3IlTfd2kACwOOAXEuhItSHmcNOV0K4M
|
||||
nI2PH6gW8HgBkWlAPm40K4jUyo3nl1usDiI6ouvYqvW7YUc2hTtPTej1l2/mS57tTt+PFurKs555
|
||||
5R9DD/xg9Nx7OuQKy5bIdlXM20UmwuZTOhRJ5kpHFRzLxaHDbSzW+orhRW4llJSevBSAH3cLOjIQ
|
||||
gh87j+MxG9j0TD2K2A0rcUcxdrnflw+mxcDVaL4payeqmOa+bJyhlftTqH+vqq5DhR68rX5VW+z7
|
||||
riqH3o8VbvO2y0XSpYHf1jowkfJj3vr8pynAUIv1dbylUSF5wtrHvzWOprw4bNrdtwQNRNy+JcVF
|
||||
dUKeNmHaL6XOe4LUWpiI11beRyCpAG52khMCEAO3Q6+4e24cEipbu6suSOtv3OpYDZeHjwNrQIhi
|
||||
rJg7i9TpMqwOeCvFWK+9UZ+P2n6h9g0/JO2+I82BFGUjVa5IvCTNOgv01GqxWY9ecdtaJjTc+dF2
|
||||
OAcRoKwvmtMJlxKEEgveui3BvPA4tuNdSrcoZBrQeo0ZHWVugXPvEZnwfZMcqwwPA+a/sUbZFg0P
|
||||
Pr0AR0ZHpytnQE9OXE8wEUgT8H1yofQ+5QoZdgMpeAb8zGs+RuviLxcDkb9NtXUAiQ49ooWuFP3L
|
||||
K9wMlaoWFTq7R+n5JVuSEYRCHC0l0bCV1/+awalT7XltXVCupI4lWzjYs52FZGGzuHG7S50Eufad
|
||||
m4CQTPVgVaVn8WW2dmpMR8Gj8WbbZdyv21wMGOWjfgT0u3oiDnddGrFOoMNnZHch6rN3FRppoh7h
|
||||
0U0fi8xxU1+EhUKq+fSIxZNr2iWN2if3Pipbxi9tyK9M41Y6aVF3HWjD58/OEql3aZjJZ1bqpXcE
|
||||
qsPeFoXX78+7mTDvL75olMk2s/mg4mLqAAWQvTuoiOmj+SgMIFuTtFR+4r/TIFNdamz6AQ3RcmWG
|
||||
ZcdRii+V27dtMA836vlAwxXRmJyE1LCL1kvUTq+J+AVsZi3xmBLFNlKPTlxswu7vSBrP1DlYOaBq
|
||||
AgA0lKnkQdeXyDk/VdbTml7ywMW1g6HkFSqKGW/IIAObmBumBcIyHE6dWEHumRQomlJssIlEFSe+
|
||||
XEQ0rwedLetJXi5A0AXT1we1wvaKCEg0Pb0ZUxygwNPDrj6MmdodH7gDfyx0mW/7mEMCtIJb5MB+
|
||||
TRGPEa/vqdJb8uGtNXUy9UlwMhJ3tYoT7NXY4+IlNjbDH/yleMdwtWP2H2WH8oC+ysXPYXjlT8eU
|
||||
poxRfJzPMVUn5SA3cvdGXDJWdX8U91j5sf9wuoYE5RBVrrJif3D3l0FpMrlWWoGw7wtZbMC2FaeT
|
||||
QvdMS5c54IoXBtBTM+/AsTAw7WEE1QSmaQGHnh6xLL5Ns8olsWeKOMlVXdO9jSDbjOGBLr7mWukW
|
||||
YzLXkH3TtJPQcbVN79af3YPhaHdMYITVKIwfg+vxZlLFHWLJQnkTl+9Qi7u2gKqkNeU7Zqs4E3CR
|
||||
9K4dHrJMyAZLZ2HA1XQEj0/tMnbTpAzZhj02JRcFobLXK9SQfw7dzGZwMRky8cHcBHoK14P5RIEV
|
||||
hr+38HSBM6wXtge5gL6DomAACvuORQO4X9x/CTjRt/J8uN3lKK5p+wi3ULeb319CEWiCiqmC1M+C
|
||||
TADUhPUhUmTinSAVkTEn+BdbH/97dVaJnvd6HtLmdSlw4xqdWUfVL9Qd7+/5L6iwlOzGLKRv97c/
|
||||
gCRw+hzXyAom+5C18slSwanMuyPgIyrrFy/kp9Romk9SQr/c0CUF2am99t8G5qvVi/TiJGHyKEXD
|
||||
aUYd4V7lqNlHMiiasvFHeq8blwmFr7rGEvbZzLNplc6sRUVlYhY2unRfyWsq9mqk3NDRW12Fa0J2
|
||||
YxQJlnXHQhNE8EyM/zsD9jCVNwsRZJ9/e5KS+ignmu6gKIR+ItDTwRfNI+NG/YmTgENUTyuO+vQC
|
||||
CUKS3PCwpP+OEC966ARl7OCMdfn1hEyiAxsZnp1RmFngR6FM+mlGgfUoWNoHvnR1/YyQ4F4dadiA
|
||||
QINwuSm5faw75F1EeL8Qi+LHKuqt05Pi/V9GJ6TzIkIsEbyyJ5sKHrp4QsU4C1p7ZhPjddz8De8k
|
||||
6ZdwMIeXxi27WKtsFLcr8JKOBe0imIilKdMBOPS31pc1iJe4472WbWM0aBwdEYmnz9+xfOqnjHtO
|
||||
0XTMjff7pzV6Y7t/u8J/zm3JS3ykote9HNRQvhZZNeVClVWd0fYFzat5ESnTojZTwHcc/BFTPnhz
|
||||
VgLyw1KEIy2r3ZyGHu1b8GSYivzl33MOK/NVBQPZUIEfdcQ5vhkAvj+Yx340IYykRFEChwioprXD
|
||||
LrIbTou7TNT5fTFA+beidHFsL+OE002/LMs6C3erSUW5C/LNjAQMS7cAV2yCyjX+/2GBmmDqnC4r
|
||||
Ja2x5yik+fbOUPh3kk/md1YvrodlX/JkQeoWRrrVJsX2dr3BgivPJavaN0Jz1eHyxAYKNqlrfd1T
|
||||
YWEDIisWerTxAVY/rEruZ6+OqLqOtZtn+4SOajOq8KFusglaMZqoYuM+LhPZck9PlZXwRqX08Vlv
|
||||
8jX5V75BFWRhFd5/LYbnQHI6ZW80Wb2sBNngLL2QJT9yXGCDJb5qCdFwGd3i655pvRJXabeyCtDD
|
||||
7I2PJcYRDd4stdq07BHyHJmye6vas8mG5QUygyWyUQv78za0m4gLMrRZBgoBDcVpWJUc+cPXzzfG
|
||||
7PvLZu/Y0SaD5hqTp0LBB1PFxTpzdVeJ21gzVNQ6D4XGLTtdv4K4fOEYoeKEuzGoBaUDtIqz47gd
|
||||
5rwfQ3ps2slkxfbtQcdKEACKvsCwzqHlgwsxD8QNOFzXYLiiiJBX22fIRoiJeSDMKSZyuFtpykCm
|
||||
7bOpybPSHv3E7EIr8sIOr9MOe/R5HSthU2IgW1L5Ynr2t9HUnCA8CenkzIQjg0h5sruxcGWCYLx7
|
||||
q0f1AQs4Z7SebVbq1SCWVJNX/vc1bVjnjYfri7RX5WMmjJkuSnuIoP6a42cqJcAg7m0STB0elFAy
|
||||
oO4vW9/JEmFUqLyQmWnoLJHX3IKtWa9CPvE=
|
||||
=OA6b
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------EOdOT2kJUL5hgCilmIhYyVZg--
|
||||
Reference in New Issue
Block a user