mirror of
https://github.com/chatmail/core.git
synced 2026-07-01 12:04:57 +03:00
Compare commits
1 Commits
adb/safe-u
...
fix_node_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008fd1cf5f |
2
.github/mergeable.yml
vendored
2
.github/mergeable.yml
vendored
@@ -22,5 +22,5 @@ mergeable:
|
||||
- do: checks
|
||||
status: 'action_required'
|
||||
payload:
|
||||
title: Changelog might need an update
|
||||
title: Changlog might need an update
|
||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -77,10 +77,10 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.60.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.60.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.56.0
|
||||
|
||||
2
.github/workflows/node-delete-preview.yml
vendored
2
.github/workflows/node-delete-preview.yml
vendored
@@ -29,4 +29,4 @@ jobs:
|
||||
host: "download.delta.chat"
|
||||
port: 22
|
||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
||||
remote: "/var/www/html/download/node/preview/"
|
||||
remote: "/var/www/html/download/node/"
|
||||
|
||||
34
.github/workflows/node-package.yml
vendored
34
.github/workflows/node-package.yml
vendored
@@ -1,15 +1,14 @@
|
||||
name: 'node.js build'
|
||||
name: 'node.js'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '!py-*'
|
||||
|
||||
|
||||
jobs:
|
||||
prebuild:
|
||||
name: 'prebuild'
|
||||
name: 'Prebuild'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -75,20 +74,6 @@ jobs:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
continue-on-error: true
|
||||
- name: Get Pullrequest ID
|
||||
id: prepare
|
||||
run: |
|
||||
tag=${{ steps.tag.outputs.tag }}
|
||||
if [ -z "$tag" ]; then
|
||||
node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||
echo "No preview will be uploaded this time, but the $tag release"
|
||||
fi
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
@@ -96,7 +81,6 @@ jobs:
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download ubuntu prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
@@ -116,17 +100,18 @@ jobs:
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
rm -rf ubuntu-18.04 macos-latest windows-latest
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
- name: build typescript part
|
||||
run: |
|
||||
npm run build:bindings:ts
|
||||
- name: Set DELTACHAT_NODE_TAR_GZ env variable
|
||||
run: |
|
||||
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
mv node/README.md README.md
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
@@ -134,10 +119,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
path: $DELTACHAT_NODE_TAR_GZ
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
|
||||
if: ${{ ! steps.tag.outputs.tag }}
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -145,12 +129,6 @@ jobs:
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
- name: "Post links to details"
|
||||
if: steps.upload-preview.outcome == 'success'
|
||||
run: node ./node/scripts/postLinksToDetails.js
|
||||
env:
|
||||
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-node build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
|
||||
67
.github/workflows/node-tests.yml
vendored
67
.github/workflows/node-tests.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: 'node.js tests'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- staging
|
||||
- trying
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: 'tests'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
|
||||
- name: Test
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Run tests on Windows, except lint
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test:mocha
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,102 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- set a default error if NDN does not provide an error
|
||||
- python: avoid exceptions when messages/contacts/chats are compared with `None`
|
||||
- python: avoid careless calls to unref functions
|
||||
|
||||
### API-Changes
|
||||
- python: added `Message.get_status_updates()` #3416
|
||||
- python: added `Message.send_status_update()` #3416
|
||||
- python: added `Message.is_webxdc()` #3416
|
||||
- python: added `Message.is_videochat_invitation()` #3416
|
||||
- python: added support for "videochat" and "webxdc" view types to `Message.new_empty()` #3416
|
||||
|
||||
## 1.86.0
|
||||
|
||||
### API-Changes
|
||||
- python: added optional `closed` parameter to `Account` constructor #3394
|
||||
- python: added optional `passphrase` parameter to `Account.export_all()` and `Account.import_all()` #3394
|
||||
- python: added `Account.open()` #3394
|
||||
- python: added `Chat.is_single()` #3394
|
||||
- python: added `Chat.is_mailinglist()` #3394
|
||||
- python: added `Chat.is_broadcast()` #3394
|
||||
- python: added `Chat.is_multiuser()` #3394
|
||||
- python: added `Chat.is_self_talk()` #3394
|
||||
- python: added `Chat.is_device_talk()` #3394
|
||||
- python: added `Chat.is_pinned()` #3394
|
||||
- python: added `Chat.pin()` #3394
|
||||
- python: added `Chat.unpin()` #3394
|
||||
- python: added `Chat.archive()` #3394
|
||||
- python: added `Chat.unarchive()` #3394
|
||||
- python: added `Message.get_summarytext()` #3394
|
||||
- python: added optional `closed` parameter to `ACFactory.get_unconfigured_account()` (pytest plugin) #3394
|
||||
- python: added optional `passphrase` parameter to `ACFactory.get_pseudo_configured_account()` (pytest plugin) #3394
|
||||
|
||||
### Changes
|
||||
- clean up series of webxdc info messages;
|
||||
`DC_EVENT_MSGS_CHANGED` is emitted on changes of existing info messages #3395
|
||||
- update provider database #3399
|
||||
- refactorings #3375 #3403 #3398 #3404
|
||||
|
||||
### Fixes
|
||||
- do not reset our database if imported backup cannot be decrypted #3397
|
||||
- node: remove `npx` from build script, this broke flathub build #3396
|
||||
|
||||
|
||||
## 1.85.0
|
||||
|
||||
### Changes
|
||||
- refactorings #3373 #3345 #3380 #3382
|
||||
- node: move split2 to devDependencies
|
||||
- python: build Python 3.10 wheels #3392
|
||||
- update Rust dependencies
|
||||
|
||||
### Fixes
|
||||
- delete outgoing MDNs found in the Sent folder on Gmail #3372
|
||||
- fix searching one-to-one chats #3377
|
||||
- do not add legacy info-messages on resending webxdc #3389
|
||||
|
||||
|
||||
## 1.84.0
|
||||
|
||||
### Changes
|
||||
- refactorings #3354 #3347 #3353 #3346
|
||||
|
||||
### Fixes
|
||||
- do not unnecessarily SELECT folders if there are no operations planned on
|
||||
them #3333
|
||||
- trim chat encryption info #3350
|
||||
- fix failure to decrypt first message to self after key synchronization
|
||||
via Autocrypt Setup Message #3352
|
||||
- Keep pgp key when you change your own email address #3351
|
||||
- Do not ignore Sent and Spam folders on Gmail #3369
|
||||
- handle decryption errors explicitly and don't get confused by encrypted mail attachments #3374
|
||||
|
||||
|
||||
## 1.83.0
|
||||
|
||||
### Fixes
|
||||
- fix node prebuild & package ci #3337
|
||||
|
||||
|
||||
## 1.82.0
|
||||
|
||||
### API-Changes
|
||||
- re-add removed `DC_MSG_ID_MARKER1` as in use on iOS #3330
|
||||
|
||||
### Changes
|
||||
- refactorings #3328
|
||||
|
||||
### Fixes
|
||||
- fix node package ci #3331
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation #3322
|
||||
|
||||
|
||||
## 1.81.0
|
||||
|
||||
### API-Changes
|
||||
@@ -121,8 +24,12 @@
|
||||
- node: throw error when getting context with an invalid account id
|
||||
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
|
||||
- use same contact-color if email address differ only in upper-/lowercase #3327
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation
|
||||
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
|
||||
|
||||
### Removed
|
||||
- node: remove unmaintained coverage scripts
|
||||
|
||||
|
||||
## 1.80.0
|
||||
|
||||
|
||||
299
Cargo.lock
generated
299
Cargo.lock
generated
@@ -230,9 +230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.7.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
|
||||
checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"futures-lite",
|
||||
@@ -279,9 +279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c"
|
||||
checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
@@ -297,7 +297,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/async-email/async-smtp?branch=master#d6b947cf2a104183fb1634dd885fc0abc2cc1f59"
|
||||
source = "git+https://github.com/async-email/async-smtp?branch=master#0cbf7043923b06612ce0dea7a3a3dae5ceaef578"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
@@ -305,7 +305,7 @@ dependencies = [
|
||||
"base64 0.13.0",
|
||||
"bufstream",
|
||||
"fast-socks5",
|
||||
"hostname",
|
||||
"hostname 0.1.5",
|
||||
"log",
|
||||
"nom 5.1.2",
|
||||
"pin-project",
|
||||
@@ -581,9 +581,9 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.10.0"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
|
||||
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "byte-pool"
|
||||
@@ -1067,7 +1067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.86.0"
|
||||
version = "1.81.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1077,6 +1077,7 @@ dependencies = [
|
||||
"async-std",
|
||||
"async-std-resolver",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"backtrace",
|
||||
"base64 0.13.0",
|
||||
"bitflags",
|
||||
@@ -1131,7 +1132,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid 1.1.1",
|
||||
"uuid",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -1145,7 +1146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.86.0"
|
||||
version = "1.81.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1262,9 +1263,9 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "1.5.2"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
|
||||
checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4"
|
||||
dependencies = [
|
||||
"signature",
|
||||
]
|
||||
@@ -1542,11 +1543,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.24"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
|
||||
checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crc32fast",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
@@ -1826,6 +1829,16 @@ dependencies = [
|
||||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winutil",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
@@ -1839,9 +1852,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-client"
|
||||
version = "6.5.2"
|
||||
version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e023af341b797ce2c039f7c6e1d347b68d0f7fd0bc7ac234fe69cfadcca1f89a"
|
||||
checksum = "ea880b03c18a7e981d7fb3608b8904a98425d53c440758fcebf7d934aa56547c"
|
||||
dependencies = [
|
||||
"async-h1",
|
||||
"async-native-tls",
|
||||
@@ -1895,7 +1908,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"termcolor",
|
||||
"toml",
|
||||
"uuid 0.8.2",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2012,15 +2025,15 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.2.6"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
|
||||
checksum = "744c24117572563a98a7e9168a5ac1ee4a1ca7f702211258797bbe0ed0346c3c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -2042,9 +2055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.2"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838"
|
||||
checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
@@ -2085,7 +2098,7 @@ dependencies = [
|
||||
"mime",
|
||||
"regex",
|
||||
"time 0.1.44",
|
||||
"uuid 0.8.2",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2103,9 +2116,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
version = "0.2.124"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2149,9 +2162,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"value-bag",
|
||||
@@ -2202,9 +2215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -2233,9 +2246,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.3"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
|
||||
checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
@@ -2363,9 +2376,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"num-traits",
|
||||
@@ -2395,9 +2408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
]
|
||||
@@ -2414,18 +2427,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.28.4"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
|
||||
checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -2441,30 +2454,18 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.40"
|
||||
version = "0.10.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
|
||||
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
@@ -2473,18 +2474,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "111.20.0+1.1.1o"
|
||||
version = "111.18.0+1.1.1n"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec"
|
||||
checksum = "7897a926e1e8d00219127dc020130eca4292e5ca666dd592480d72c3eca2ff6c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.73"
|
||||
version = "0.9.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
|
||||
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"cc",
|
||||
@@ -2554,12 +2555,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.3",
|
||||
"parking_lot_core 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2578,15 +2579,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.3"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||
checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys 0.36.1",
|
||||
"windows-sys 0.34.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2806,11 +2807,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
|
||||
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2850,9 +2851,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.23.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc"
|
||||
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2992,9 +2993,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.5.3"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
|
||||
checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"crossbeam-deque",
|
||||
@@ -3004,9 +3005,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.9.3"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
|
||||
checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
@@ -3036,9 +3037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.6"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -3053,9 +3054,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.26"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
@@ -3072,7 +3073,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"hostname 0.3.1",
|
||||
"quick-error 1.2.3",
|
||||
]
|
||||
|
||||
@@ -3151,14 +3152,14 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 1.0.9",
|
||||
"semver 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.34.8"
|
||||
version = "0.34.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2079c267b8394eb529872c3cf92e181c378b41fea36e68130357b52493701d2e"
|
||||
checksum = "3d275ec42d7bd48e2863912dd8075a4d7376c6f1433b922a0175578b5c7725ce"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@@ -3200,9 +3201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.10"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
@@ -3221,9 +3222,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.4.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
|
||||
checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
@@ -3231,21 +3232,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.20"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
|
||||
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.36.1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.6"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
|
||||
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
|
||||
dependencies = [
|
||||
"parking_lot 0.12.1",
|
||||
"parking_lot 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3288,9 +3289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.9"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
|
||||
checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4"
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
@@ -3300,9 +3301,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3319,9 +3320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3330,11 +3331,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.81"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
dependencies = [
|
||||
"itoa 1.0.2",
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -3357,7 +3358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.2",
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -3439,9 +3440,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.14"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
|
||||
checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
@@ -3600,9 +3601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "str-buf"
|
||||
version = "1.0.6"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
|
||||
checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
@@ -3659,13 +3660,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.95"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
|
||||
checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3737,18 +3738,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.31"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.31"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3831,9 +3832,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.18.2"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
|
||||
checksum = "0f48b6d60512a392e34dbf7fd456249fd2de3c83669ab642e021903f4015185b"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -3883,7 +3884,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lru-cache",
|
||||
"parking_lot 0.12.1",
|
||||
"parking_lot 0.12.0",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@@ -3931,12 +3932,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.2"
|
||||
@@ -3969,9 +3964,9 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.3"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
@@ -4007,15 +4002,6 @@ name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238"
|
||||
dependencies = [
|
||||
"getrandom 0.2.6",
|
||||
"serde",
|
||||
@@ -4023,9 +4009,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.0.0-alpha.9"
|
||||
version = "1.0.0-alpha.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
|
||||
checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"version_check 0.9.4",
|
||||
@@ -4221,15 +4207,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc 0.36.1",
|
||||
"windows_i686_gnu 0.36.1",
|
||||
"windows_i686_msvc 0.36.1",
|
||||
"windows_x86_64_gnu 0.36.1",
|
||||
"windows_x86_64_msvc 0.36.1",
|
||||
"windows_aarch64_msvc 0.34.0",
|
||||
"windows_i686_gnu 0.34.0",
|
||||
"windows_i686_msvc 0.34.0",
|
||||
"windows_x86_64_gnu 0.34.0",
|
||||
"windows_x86_64_msvc 0.34.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4240,9 +4226,9 @@ checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
@@ -4252,9 +4238,9 @@ checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
@@ -4264,9 +4250,9 @@ checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
@@ -4276,9 +4262,9 @@ checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
@@ -4288,9 +4274,9 @@ checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
@@ -4301,6 +4287,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winutil"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.2.0"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.86.0"
|
||||
version = "1.81.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -25,6 +25,7 @@ async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master
|
||||
async-std-resolver = "0.21"
|
||||
async-std = { version = "1" }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.3"
|
||||
@@ -45,11 +46,11 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.12.0"
|
||||
once_cell = "1.10.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.23"
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.7"
|
||||
@@ -57,7 +58,7 @@ regex = "1.5"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.4"
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
@@ -69,7 +70,7 @@ surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.86.0"
|
||||
version = "1.81.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![deny(unused, clippy::all)]
|
||||
#![deny(clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#[macro_use]
|
||||
extern crate human_panic;
|
||||
extern crate num_traits;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
@@ -456,37 +458,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
}
|
||||
|
||||
let event = &*event;
|
||||
match event.typ {
|
||||
EventType::Info(_) => 100,
|
||||
EventType::SmtpConnected(_) => 101,
|
||||
EventType::ImapConnected(_) => 102,
|
||||
EventType::SmtpMessageSent(_) => 103,
|
||||
EventType::ImapMessageDeleted(_) => 104,
|
||||
EventType::ImapMessageMoved(_) => 105,
|
||||
EventType::NewBlobFile(_) => 150,
|
||||
EventType::DeletedBlobFile(_) => 151,
|
||||
EventType::Warning(_) => 300,
|
||||
EventType::Error(_) => 400,
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
EventType::MsgRead { .. } => 2015,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
EventType::ImexProgress(_) => 2051,
|
||||
EventType::ImexFileWritten(_) => 2052,
|
||||
EventType::SecurejoinInviterProgress { .. } => 2060,
|
||||
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
||||
EventType::ConnectivityChanged => 2100,
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
}
|
||||
event.as_id()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3574,8 +3546,7 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
|
||||
ffi_msg
|
||||
.message
|
||||
.latefiling_mediasize(ctx, width, height, duration)
|
||||
})
|
||||
.ok_or_log_msg(ctx, "Cannot set media size");
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more pepole know what old addresses you had).
|
||||
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
|
||||
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
|
||||
|
||||
#### Downsides of this design:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- Faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
|
||||
|
||||
### Alternatives and old discussions/plans:
|
||||
|
||||
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
|
||||
|
||||
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the neccessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
|
||||
|
||||
- The condition for a transition temporarily was:
|
||||
|
||||
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
|
||||
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
|
||||
> AND ...
|
||||
|
||||
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
|
||||
|
||||
<details>
|
||||
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
|
||||
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
|
||||
|
||||
1. (DONE) At the time of configure we push the current primary to become a secondary.
|
||||
|
||||
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
|
||||
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
|
||||
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
|
||||
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
|
||||
|
||||
Notes:
|
||||
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
|
||||
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
|
||||
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
|
||||
- there are no extra flags/columns in the database needed (i hope)
|
||||
|
||||
#### Downsides of the chosen approach:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
|
||||
|
||||
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
_end of the previous state of the discussion_
|
||||
|
||||
</details>
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
33
draft/aeap-mvp.rst
Normal file
33
draft/aeap-mvp.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
@@ -1,12 +1,6 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
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
|
||||
when developing Webxdc apps.
|
||||
|
||||
(This document may eventually be merged with the [webxdc guidebook](https://deltachat.github.io/webxdc_docs/), where you may currently find other useful information.)
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
@@ -40,9 +34,8 @@ To get a shared state, the peers use `sendUpdate()` to send updates to each othe
|
||||
- `update`: an object with the following properties:
|
||||
- `update.payload`: any javascript primitive, array or object.
|
||||
- `update.info`: optional, short, informational message that will be added to the chat,
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame".
|
||||
usually only one line of text is shown
|
||||
and if there are series of info messages, older ones may be dropped.
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
usually only one line of text is shown,
|
||||
use this option sparingly to not spam the chat.
|
||||
- `update.document`: optional, name of the document in edit,
|
||||
must not be used eg. in games where the Webxdc does not create documents
|
||||
@@ -144,34 +137,6 @@ round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
|
||||
|
||||
## Other APIs and Tags Usage Hints
|
||||
|
||||
- `localStorage`, `sessionStorage`, `indexedDB` are okay to be used
|
||||
- `visibilitychange`-events are okay to be used
|
||||
- `window.navigator.language` is okay to be used, on desktop, this is currently always "en-GB"
|
||||
- `<a href="localfile.html">` and other internal links are okay to be used
|
||||
- `<a href="mailto:addr@example.org?body=...">`-links are okay to be used
|
||||
- `<meta name="viewport" ...>` usage is okay to be used
|
||||
and useful esp. different webviews have different defaults
|
||||
|
||||
|
||||
### Discouraged Things
|
||||
|
||||
- `document.cookie` is known not to work on desktop and iOS
|
||||
use `localStorage` instead
|
||||
- `unload`-, `beforeunload`- and `pagehide`-events are known not to work on iOS and are flaky on other systems
|
||||
(also partly discouraged by [mozilla](https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event))
|
||||
use `visibilitychange` instead
|
||||
- `<title>` and `document.title` is ignored by Webxdc;
|
||||
use the `name` property from `manifest.toml` instead
|
||||
- newest js features may not work on all webviews,
|
||||
you may want to transpile your code down to an older js version
|
||||
eg. with <https://babeljs.io>
|
||||
- `<a href="https://example.org/foo">` and other external links are blocked by definition;
|
||||
instead, embed content or use `mailto:` link to offer a way for contact
|
||||
- `<input type="file">` is discouraged currently; this may change in future
|
||||
|
||||
|
||||
## Webxdc Examples
|
||||
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
@@ -204,10 +169,30 @@ The following example shows an input field and every input is show on all peers
|
||||
</html>
|
||||
```
|
||||
|
||||
More examples at [github.com/webxdc](https://github.com/webxdc) and
|
||||
[topic #webxdc](https://github.com/topics/webxdc)
|
||||
|
||||
[github.com/webxdc/hello](https://github.com/webxdc/hello)
|
||||
offers an **Webxdc Tool** that can be used in many browsers without any installation needed.
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own Webxdc -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc) or in the [webxdc GitHub organization](https://github.com/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
|
||||
- older devices might not have the newest js features in their webview,
|
||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
||||
- viewport and scaling features are implementation specific,
|
||||
if you want to have an explicit behavior, you can add eg.
|
||||
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
|
||||
- the `<title>` tag should not be used and its content is usually not displayed;
|
||||
instead, use the `name` property from `manifest.toml`
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
|
||||
@@ -17,7 +17,7 @@ const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url,
|
||||
target_url: base_url + file_url + '.tar.gz',
|
||||
}
|
||||
|
||||
const http = require('https')
|
||||
|
||||
@@ -326,7 +326,7 @@ static void call_js_event_handler(napi_env env, napi_value js_callback, void* _c
|
||||
|
||||
if (status != napi_ok) {
|
||||
TRACE("Unable to call event_handler callback2");
|
||||
const napi_extended_error_info* error_result;
|
||||
napi_extended_error_info* error_result;
|
||||
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
|
||||
}
|
||||
}
|
||||
@@ -3195,7 +3195,7 @@ static void call_accounts_js_event_handler(napi_env env, napi_value js_callback,
|
||||
|
||||
if (status != napi_ok) {
|
||||
TRACE("Unable to call event_handler callback2");
|
||||
const napi_extended_error_info* error_result;
|
||||
napi_extended_error_info* error_result;
|
||||
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0"
|
||||
"node-gyp-build": "^4.1.0",
|
||||
"split2": "^3.1.1"
|
||||
},
|
||||
"description": "node.js bindings for deltachat-core",
|
||||
"devDependencies": {
|
||||
@@ -19,7 +20,6 @@
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"split2": "^4.1.0",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
@@ -42,7 +42,7 @@
|
||||
"build": "npm run build:core && npm run build:bindings",
|
||||
"build:bindings": "npm run build:bindings:c && npm run build:bindings:ts",
|
||||
"build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall",
|
||||
"build:bindings:c:c": "cd node && node-gyp rebuild",
|
||||
"build:bindings:c:c": "cd node && npx node-gyp rebuild",
|
||||
"build:bindings:c:postinstall": "node node/scripts/postinstall.js",
|
||||
"build:bindings:ts": "cd node && tsc",
|
||||
"build:core": "npm run build:core:rust && npm run build:core:constants",
|
||||
@@ -61,5 +61,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.86.0"
|
||||
"version": "1.81.0"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
# content of echo_and_quit.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
# content of group_tracking.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
@@ -32,21 +33,15 @@ class GroupTrackingPlugin:
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
)
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
)
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
|
||||
import pytest
|
||||
import py
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from deltachat.events import FFIEventLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope='session')
|
||||
def datadir():
|
||||
"""The py.path.local object of the test-data/ directory."""
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
datadir = path.join('test-data')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip("test-data directory not found")
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
@@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
@@ -44,11 +44,9 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_configure_completed*
|
||||
"""
|
||||
)
|
||||
""")
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
@@ -58,11 +56,9 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_chat_modified*bot test group*
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
||||
@@ -72,20 +68,12 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
assert "hello" in reply.text
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
)
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
)
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
|
||||
@@ -6,9 +6,3 @@ build-backend = "setuptools.build_meta"
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
@@ -1,15 +1,15 @@
|
||||
import sys
|
||||
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
from . import capi, const, events, hookspec # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from . import capi, const, hookspec # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .chat import Chat # noqa
|
||||
from .contact import Contact # noqa
|
||||
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from .message import Message # noqa
|
||||
from .contact import Contact # noqa
|
||||
from .chat import Chat # noqa
|
||||
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
||||
from . import events
|
||||
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
try:
|
||||
__version__ = get_distribution(__name__).version
|
||||
except DistributionNotFound:
|
||||
@@ -26,7 +26,7 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
|
||||
|
||||
def register_global_plugin(plugin):
|
||||
"""Register a global plugin which implements one or more
|
||||
""" Register a global plugin which implements one or more
|
||||
of the :class:`deltachat.hookspec.Global` hooks.
|
||||
"""
|
||||
gm = hookspec.Global._get_plugin_manager()
|
||||
@@ -43,10 +43,9 @@ register_global_plugin(events)
|
||||
|
||||
|
||||
def run_cmdline(argv=None, account_plugins=None):
|
||||
"""Run a simple default command line app, registering the specified
|
||||
account plugins."""
|
||||
""" Run a simple default command line app, registering the specified
|
||||
account plugins. """
|
||||
import argparse
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
@@ -70,9 +69,9 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
ac.add_account_plugin(plugin)
|
||||
|
||||
if not ac.is_configured():
|
||||
assert (
|
||||
args.email and args.password
|
||||
), "you must specify --email and --password once to configure this database/account"
|
||||
assert args.email and args.password, (
|
||||
"you must specify --email and --password once to configure this database/account"
|
||||
)
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
|
||||
@@ -20,33 +20,30 @@ def local_build_flags(projdir, target):
|
||||
:param target: The rust build target, `debug` or `release`.
|
||||
"""
|
||||
flags = {}
|
||||
if platform.system() == "Darwin":
|
||||
flags["libraries"] = ["resolv", "dl"]
|
||||
flags["extra_link_args"] = [
|
||||
"-framework",
|
||||
"CoreFoundation",
|
||||
"-framework",
|
||||
"CoreServices",
|
||||
"-framework",
|
||||
"Security",
|
||||
if platform.system() == 'Darwin':
|
||||
flags['libraries'] = ['resolv', 'dl']
|
||||
flags['extra_link_args'] = [
|
||||
'-framework', 'CoreFoundation',
|
||||
'-framework', 'CoreServices',
|
||||
'-framework', 'Security',
|
||||
]
|
||||
elif platform.system() == "Linux":
|
||||
flags["libraries"] = ["rt", "dl", "m"]
|
||||
flags["extra_link_args"] = []
|
||||
elif platform.system() == 'Linux':
|
||||
flags['libraries'] = ['rt', 'dl', 'm']
|
||||
flags['extra_link_args'] = []
|
||||
else:
|
||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
||||
if target_dir is None:
|
||||
target_dir = os.path.join(projdir, "target")
|
||||
flags["extra_objects"] = [os.path.join(target_dir, target, "libdeltachat.a")]
|
||||
assert os.path.exists(flags["extra_objects"][0]), flags["extra_objects"]
|
||||
flags["include_dirs"] = [os.path.join(projdir, "deltachat-ffi")]
|
||||
target_dir = os.path.join(projdir, 'target')
|
||||
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects']
|
||||
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
return flags
|
||||
|
||||
|
||||
def system_build_flags():
|
||||
"""Construct build flags for building against an installed libdeltachat."""
|
||||
return pkgconfig.parse("deltachat")
|
||||
return pkgconfig.parse('deltachat')
|
||||
|
||||
|
||||
def extract_functions(flags):
|
||||
@@ -64,13 +61,11 @@ def extract_functions(flags):
|
||||
src_name = os.path.join(tmpdir, "include.h")
|
||||
dst_name = os.path.join(tmpdir, "expanded.h")
|
||||
with open(src_name, "w") as src_fp:
|
||||
src_fp.write("#include <deltachat.h>")
|
||||
cc.preprocess(
|
||||
source=src_name,
|
||||
output_file=dst_name,
|
||||
include_dirs=flags["include_dirs"],
|
||||
macros=[("PY_CFFI", "1")],
|
||||
)
|
||||
src_fp.write('#include <deltachat.h>')
|
||||
cc.preprocess(source=src_name,
|
||||
output_file=dst_name,
|
||||
include_dirs=flags['include_dirs'],
|
||||
macros=[('PY_CFFI', '1')])
|
||||
with open(dst_name, "r") as dst_fp:
|
||||
return dst_fp.read()
|
||||
finally:
|
||||
@@ -92,9 +87,7 @@ def find_header(flags):
|
||||
obj_name = os.path.join(tmpdir, "where.o")
|
||||
dst_name = os.path.join(tmpdir, "where")
|
||||
with open(src_name, "w") as src_fp:
|
||||
src_fp.write(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
src_fp.write(textwrap.dedent("""
|
||||
#include <stdio.h>
|
||||
#include <deltachat.h>
|
||||
|
||||
@@ -102,20 +95,18 @@ def find_header(flags):
|
||||
printf("%s", _dc_header_file_location());
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
"""))
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
cc.compile(
|
||||
sources=["where.c"],
|
||||
include_dirs=flags["include_dirs"],
|
||||
macros=[("PY_CFFI_INC", "1")],
|
||||
)
|
||||
cc.compile(sources=["where.c"],
|
||||
include_dirs=flags['include_dirs'],
|
||||
macros=[("PY_CFFI_INC", "1")])
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
cc.link_executable(objects=[obj_name], output_progname="where", output_dir=tmpdir)
|
||||
cc.link_executable(objects=[obj_name],
|
||||
output_progname="where",
|
||||
output_dir=tmpdir)
|
||||
return subprocess.check_output(dst_name)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
@@ -132,8 +123,7 @@ def extract_defines(flags):
|
||||
cdef() method.
|
||||
"""
|
||||
header = find_header(flags)
|
||||
defines_re = re.compile(
|
||||
r"""
|
||||
defines_re = re.compile(r"""
|
||||
\#define\s+ # The start of a define.
|
||||
( # Begin capturing group which captures the define name.
|
||||
(?: # A nested group which is not captured, this allows us
|
||||
@@ -161,28 +151,26 @@ def extract_defines(flags):
|
||||
) # Close the capturing group, this contains
|
||||
# the entire name e.g. DC_MSG_TEXT.
|
||||
\s+\S+ # Ensure there is whitespace followed by a value.
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
""", re.VERBOSE)
|
||||
defines = []
|
||||
with open(header) as fp:
|
||||
for line in fp:
|
||||
match = defines_re.match(line)
|
||||
if match:
|
||||
defines.append(match.group(1))
|
||||
return "\n".join("#define {} ...".format(d) for d in defines)
|
||||
return '\n'.join('#define {} ...'.format(d) for d in defines)
|
||||
|
||||
|
||||
def ffibuilder():
|
||||
projdir = os.environ.get("DCC_RS_DEV")
|
||||
projdir = os.environ.get('DCC_RS_DEV')
|
||||
if projdir:
|
||||
target = os.environ.get("DCC_RS_TARGET", "release")
|
||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||
flags = local_build_flags(projdir, target)
|
||||
else:
|
||||
flags = system_build_flags()
|
||||
builder = cffi.FFI()
|
||||
builder.set_source(
|
||||
"deltachat.capi",
|
||||
'deltachat.capi',
|
||||
"""
|
||||
#include <deltachat.h>
|
||||
int dc_event_has_string_data(int e)
|
||||
@@ -192,13 +180,11 @@ def ffibuilder():
|
||||
""",
|
||||
**flags,
|
||||
)
|
||||
builder.cdef(
|
||||
"""
|
||||
builder.cdef("""
|
||||
typedef int... time_t;
|
||||
void free(void *ptr);
|
||||
extern int dc_event_has_string_data(int);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
function_defs = extract_functions(flags)
|
||||
defines = extract_defines(flags)
|
||||
builder.cdef(function_defs)
|
||||
@@ -206,9 +192,8 @@ def ffibuilder():
|
||||
return builder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
import os.path
|
||||
|
||||
pkgdir = os.path.join(os.path.dirname(__file__), "..")
|
||||
pkgdir = os.path.join(os.path.dirname(__file__), '..')
|
||||
builder = ffibuilder()
|
||||
builder.compile(tmpdir=pkgdir, verbose=True)
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
""" Account class implementation. """
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
from array import array
|
||||
from contextlib import contextmanager
|
||||
from email.utils import parseaddr
|
||||
from threading import Event
|
||||
from typing import Any, Dict, Generator, List, Optional, Union
|
||||
|
||||
from . import const, hookspec
|
||||
import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .contact import Contact
|
||||
from .cutil import (
|
||||
DCLot,
|
||||
as_dc_charpointer,
|
||||
from_dc_charpointer,
|
||||
from_optional_dc_charpointer,
|
||||
iter_array,
|
||||
safe_unref,
|
||||
)
|
||||
from .events import EventThread
|
||||
from .message import Message
|
||||
from .tracker import ConfigureTracker, ImexTracker
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
from typing import Union, Any, Dict, Optional, List, Generator
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
"""Account is missing `addr` and `mail_pw` config values."""
|
||||
""" Account is missing `addr` and `mail_pw` config values. """
|
||||
|
||||
|
||||
def get_core_info():
|
||||
"""get some system info."""
|
||||
""" get some system info. """
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile() as path:
|
||||
path.close()
|
||||
return get_dc_info_as_dict(
|
||||
ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
safe_unref(lib.dc_context_unref),
|
||||
)
|
||||
)
|
||||
return get_dc_info_as_dict(ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
))
|
||||
|
||||
|
||||
def get_dc_info_as_dict(dc_context):
|
||||
@@ -56,22 +46,18 @@ def get_dc_info_as_dict(dc_context):
|
||||
|
||||
|
||||
class Account(object):
|
||||
"""Each account is tied to a sqlite database file which is fully managed
|
||||
""" Each account is tied to a sqlite database file which is fully managed
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
meant to be memory-safe and return memory-safe objects.
|
||||
"""
|
||||
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True, closed=False) -> None:
|
||||
"""initialize account object.
|
||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
will be created if it doesn't exist.
|
||||
:param os_name: [Deprecated]
|
||||
:param logging: enable logging for this account
|
||||
:param closed: set to True to avoid automatically opening the account
|
||||
after creation.
|
||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||
"""
|
||||
# initialize per-account plugin system
|
||||
self._pm = hookspec.PerAccount._make_plugin_manager()
|
||||
@@ -84,8 +70,8 @@ class Account(object):
|
||||
db_path = db_path.encode("utf8")
|
||||
|
||||
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),
|
||||
safe_unref(lib.dc_context_unref),
|
||||
lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
if self._dc_context == ffi.NULL:
|
||||
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
|
||||
@@ -96,23 +82,12 @@ class Account(object):
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def open(self, passphrase: Optional[str] = None) -> bool:
|
||||
"""Open the account's database with the given passphrase.
|
||||
This can only be used on a closed account. If the account is new, this
|
||||
operation sets the database passphrase. For existing databases the passphrase
|
||||
should be the one used to encrypt the database the first time.
|
||||
|
||||
:returns: True if the database is opened with this passphrase, False if the
|
||||
passphrase is incorrect or an error occurred.
|
||||
"""
|
||||
return bool(lib.dc_context_open(self._dc_context, as_dc_charpointer(passphrase)))
|
||||
|
||||
def disable_logging(self) -> None:
|
||||
"""disable logging."""
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self) -> None:
|
||||
"""re-enable logging."""
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
def __repr__(self):
|
||||
@@ -127,10 +102,11 @@ class Account(object):
|
||||
|
||||
def _check_config_key(self, name: str) -> None:
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(name, self._configkeys))
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
"""return dictionary of built config parameters."""
|
||||
""" return dictionary of built config parameters. """
|
||||
return get_dc_info_as_dict(self._dc_context)
|
||||
|
||||
def dump_account_info(self, logfile):
|
||||
@@ -150,7 +126,7 @@ class Account(object):
|
||||
log("")
|
||||
|
||||
def set_stock_translation(self, id: int, string: str) -> None:
|
||||
"""set stock translation string.
|
||||
""" set stock translation string.
|
||||
|
||||
:param id: id of stock string (const.DC_STR_*)
|
||||
:param value: string to set as new transalation
|
||||
@@ -162,7 +138,7 @@ class Account(object):
|
||||
raise ValueError("could not set translation string")
|
||||
|
||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
||||
"""set configuration values.
|
||||
""" set configuration values.
|
||||
|
||||
:param name: config key name (unicode)
|
||||
:param value: value to set (unicode)
|
||||
@@ -181,7 +157,7 @@ class Account(object):
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name: str) -> str:
|
||||
"""return unicode string value.
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
:returns: unicode value
|
||||
@@ -199,17 +175,15 @@ class Account(object):
|
||||
|
||||
In other words, you don't need this.
|
||||
"""
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
as_dc_charpointer(public),
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
res = lib.dc_preconfigure_keypair(self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
as_dc_charpointer(public),
|
||||
as_dc_charpointer(secret))
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
||||
"""update config values.
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
values need to be unicode.
|
||||
@@ -219,18 +193,18 @@ class Account(object):
|
||||
self.set_config(key, value)
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""determine if the account is configured already; an initial connection
|
||||
""" determine if the account is configured already; an initial connection
|
||||
to SMTP/IMAP has been verified.
|
||||
|
||||
:returns: True if account is configured.
|
||||
"""
|
||||
return bool(lib.dc_is_configured(self._dc_context))
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
|
||||
:returns True if account is open."""
|
||||
return bool(lib.dc_context_is_open(self._dc_context))
|
||||
return True if lib.dc_context_is_open(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
@@ -245,17 +219,18 @@ class Account(object):
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
def check_is_configured(self) -> None:
|
||||
"""Raise ValueError if this account is not configured."""
|
||||
""" Raise ValueError if this account is not configured. """
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
||||
"""return the latest backup file in a given directory."""
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self) -> str:
|
||||
"""return the directory for files.
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
Place files there directly to avoid copying.
|
||||
@@ -263,7 +238,7 @@ class Account(object):
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self) -> Contact:
|
||||
"""return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
@@ -305,14 +280,14 @@ class Account(object):
|
||||
elif isinstance(obj, str):
|
||||
displayname, addr = parseaddr(obj)
|
||||
else:
|
||||
raise TypeError("don't know how to create chat for %r" % (obj,))
|
||||
raise TypeError("don't know how to create chat for %r" % (obj, ))
|
||||
|
||||
if name is None and displayname:
|
||||
name = displayname
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact: Contact) -> bool:
|
||||
"""delete a Contact.
|
||||
""" delete a Contact.
|
||||
|
||||
:param contact: contact object obtained
|
||||
:returns: True if deletion succeeded (contact was deleted)
|
||||
@@ -323,7 +298,7 @@ class Account(object):
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
@@ -332,18 +307,21 @@ class Account(object):
|
||||
return None
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""return Contact instance or raise an exception.
|
||||
""" return Contact instance or raise an exception.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self) -> List[Contact]:
|
||||
"""return a list of all blocked contacts.
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
"""
|
||||
dc_array = ffi.gc(lib.dc_get_blocked_contacts(self._dc_context), safe_unref(lib.dc_array_unref))
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_blocked_contacts(self._dc_context),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_contacts(
|
||||
@@ -366,16 +344,22 @@ class Account(object):
|
||||
flags |= const.DC_GCL_VERIFIED_ONLY
|
||||
if with_self:
|
||||
flags |= const.DC_GCL_ADD_SELF
|
||||
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query), safe_unref(lib.dc_array_unref))
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
"""yield all fresh messages from all chats."""
|
||||
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), safe_unref(lib.dc_array_unref))
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def create_group_chat(
|
||||
@@ -401,11 +385,14 @@ class Account(object):
|
||||
return chat
|
||||
|
||||
def get_chats(self) -> List[Chat]:
|
||||
"""return list of chats.
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
"""
|
||||
dc_chatlist = ffi.gc(lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), safe_unref(lib.dc_chatlist_unref))
|
||||
dc_chatlist = ffi.gc(
|
||||
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
|
||||
lib.dc_chatlist_unref
|
||||
)
|
||||
|
||||
assert dc_chatlist != ffi.NULL
|
||||
chatlist = []
|
||||
@@ -418,14 +405,14 @@ class Account(object):
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
"""return Message instance.
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
"""return Chat instance.
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
:raises: ValueError if chat does not exist.
|
||||
@@ -437,7 +424,7 @@ class Account(object):
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
||||
"""mark the given set of messages as seen.
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
:param messages: a list of message ids or Message instances.
|
||||
"""
|
||||
@@ -451,7 +438,7 @@ class Account(object):
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
||||
"""Forward list of messages to a chat.
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
:param chat: :class:`deltachat.chat.Chat` object.
|
||||
@@ -461,7 +448,7 @@ class Account(object):
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""delete messages (local and remote).
|
||||
""" delete messages (local and remote).
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
:returns: None
|
||||
@@ -470,34 +457,31 @@ class Account(object):
|
||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||
|
||||
def export_self_keys(self, path):
|
||||
"""export public and private keys to the specified directory.
|
||||
""" export public and private keys to the specified directory.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
return self._export(path, imex_cmd=const.DC_IMEX_EXPORT_SELF_KEYS)
|
||||
|
||||
def export_all(self, path: str, passphrase: Optional[str] = None) -> str:
|
||||
"""Export a backup of all database state (chats, contacts, keys, media, ...).
|
||||
|
||||
:param path: the directory where the backup will be stored.
|
||||
:param passphrase: the backup will be encrypted with the passphrase, if it is
|
||||
None or empty string, the backup is not encrypted.
|
||||
:returns: path to the created backup.
|
||||
def export_all(self, path):
|
||||
"""return new file containing a backup of all database state
|
||||
(chats, contacts, keys, media, ...). The file is created in the
|
||||
the `path` directory.
|
||||
|
||||
Note that the account has to be stopped; call stop_io() if necessary.
|
||||
"""
|
||||
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP, passphrase)
|
||||
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP)
|
||||
if len(export_files) != 1:
|
||||
raise RuntimeError("found more than one new file")
|
||||
return export_files[0]
|
||||
|
||||
def _export(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> list:
|
||||
def _export(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
self.imex(path, imex_cmd, passphrase)
|
||||
self.imex(path, imex_cmd)
|
||||
return imex_tracker.wait_finish()
|
||||
|
||||
def import_self_keys(self, path):
|
||||
"""Import private keys found in the `path` directory.
|
||||
""" Import private keys found in the `path` directory.
|
||||
The last imported key is made the default keys unless its name
|
||||
contains the string legacy. Public keys are not imported.
|
||||
|
||||
@@ -505,23 +489,21 @@ class Account(object):
|
||||
"""
|
||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_SELF_KEYS)
|
||||
|
||||
def import_all(self, path: str, passphrase: Optional[str] = None) -> None:
|
||||
"""Import Delta Chat state from the specified backup file.
|
||||
The account must be in unconfigured state for import to attempted.
|
||||
def import_all(self, path):
|
||||
"""import delta chat state from the specified backup `path` (a file).
|
||||
|
||||
:param path: path to the backup file.
|
||||
:param passphrase: if not None or empty, the backup will be opened with the passphrase.
|
||||
The account must be in unconfigured state for import to attempted.
|
||||
"""
|
||||
assert not self.is_configured(), "cannot import into configured account"
|
||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_BACKUP, passphrase=passphrase)
|
||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_BACKUP)
|
||||
|
||||
def _import(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
||||
def _import(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
self.imex(path, imex_cmd, passphrase)
|
||||
self.imex(path, imex_cmd)
|
||||
imex_tracker.wait_finish()
|
||||
|
||||
def imex(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), as_dc_charpointer(passphrase))
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
@@ -535,7 +517,7 @@ class Account(object):
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
"""get/create Setup-Contact QR Code as ascii-string.
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||
@@ -545,15 +527,18 @@ class Account(object):
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def check_qr(self, qr):
|
||||
"""check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
||||
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), safe_unref(lib.dc_lot_unref))
|
||||
""" check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
||||
res = ffi.gc(
|
||||
lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)),
|
||||
lib.dc_lot_unref
|
||||
)
|
||||
lot = DCLot(res)
|
||||
if lot.state() == const.DC_QR_ERROR:
|
||||
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
|
||||
return ScannedQRCode(lot)
|
||||
|
||||
def qr_setup_contact(self, qr):
|
||||
"""setup contact and return a Chat after contact is established.
|
||||
""" setup contact and return a Chat after contact is established.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
@@ -567,7 +552,7 @@ class Account(object):
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def qr_join_chat(self, qr):
|
||||
"""join a chat group through a QR code.
|
||||
""" join a chat group through a QR code.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
@@ -581,7 +566,9 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def set_location(self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0) -> None:
|
||||
def set_location(
|
||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
||||
) -> None:
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
@@ -600,7 +587,7 @@ class Account(object):
|
||||
#
|
||||
|
||||
def add_account_plugin(self, plugin, name=None):
|
||||
"""add an account plugin which implements one or more of
|
||||
""" add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
"""
|
||||
if name and self._pm.has_plugin(name=name):
|
||||
@@ -610,18 +597,18 @@ class Account(object):
|
||||
return plugin
|
||||
|
||||
def remove_account_plugin(self, plugin, name=None):
|
||||
"""remove an account plugin."""
|
||||
""" remove an account plugin. """
|
||||
self._pm.unregister(plugin, name=name)
|
||||
|
||||
@contextmanager
|
||||
def temp_plugin(self, plugin):
|
||||
"""run a with-block with the given plugin temporarily registered."""
|
||||
""" run a with-block with the given plugin temporarily registered. """
|
||||
self._pm.register(plugin)
|
||||
yield plugin
|
||||
self._pm.unregister(plugin)
|
||||
|
||||
def stop_ongoing(self):
|
||||
"""Stop ongoing securejoin, configuration or other core jobs."""
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
def get_connectivity(self):
|
||||
@@ -634,7 +621,7 @@ class Account(object):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
"""start this account's IO scheduling (Rust-core async scheduler)
|
||||
""" start this account's IO scheduling (Rust-core async scheduler)
|
||||
|
||||
If this account is not configured an Exception is raised.
|
||||
You need to call account.configure() and account.wait_configure_finish()
|
||||
@@ -678,7 +665,7 @@ class Account(object):
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
"""Start configuration process and return a Configtracker instance
|
||||
""" Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
"""
|
||||
@@ -691,11 +678,11 @@ class Account(object):
|
||||
return configtracker
|
||||
|
||||
def wait_shutdown(self) -> None:
|
||||
"""wait until shutdown of this account has completed."""
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def stop_io(self) -> None:
|
||||
"""stop core IO scheduler if it is running."""
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
|
||||
@@ -703,7 +690,7 @@ class Account(object):
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""shutdown and destroy account (stop callback thread, close and remove
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
return
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
""" Chat and Location related API. """
|
||||
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import (
|
||||
as_dc_charpointer,
|
||||
from_dc_charpointer,
|
||||
from_optional_dc_charpointer,
|
||||
iter_array,
|
||||
safe_unref,
|
||||
)
|
||||
from .message import Message
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Chat(object):
|
||||
"""Chat object which manages members and through which you can send and retrieve messages.
|
||||
""" Chat object which manages members and through which you can send and retrieve messages.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id) -> None:
|
||||
from .account import Account
|
||||
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
@@ -45,7 +36,10 @@ class Chat(object):
|
||||
|
||||
@property
|
||||
def _dc_chat(self):
|
||||
return ffi.gc(lib.dc_get_chat(self.account._dc_context, self.id), safe_unref(lib.dc_chat_unref))
|
||||
return ffi.gc(
|
||||
lib.dc_get_chat(self.account._dc_context, self.id),
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
@@ -68,66 +62,28 @@ class Chat(object):
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self) -> bool:
|
||||
"""Return True if this chat is a group chat.
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, False otherwise
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_single(self) -> bool:
|
||||
"""Return True if this chat is a single/direct chat, False otherwise"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
|
||||
|
||||
def is_mailinglist(self) -> bool:
|
||||
"""Return True if this chat is a mailing list, False otherwise"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST
|
||||
|
||||
def is_broadcast(self) -> bool:
|
||||
"""Return True if this chat is a broadcast list, False otherwise"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST
|
||||
|
||||
def is_multiuser(self) -> bool:
|
||||
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_MAILINGLIST,
|
||||
const.DC_CHAT_TYPE_BROADCAST,
|
||||
)
|
||||
|
||||
def is_self_talk(self) -> bool:
|
||||
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise"""
|
||||
return bool(lib.dc_chat_is_self_talk(self._dc_chat))
|
||||
|
||||
def is_device_talk(self) -> bool:
|
||||
"""Returns True if this chat is the "Device Messages" chat, False otherwise"""
|
||||
return bool(lib.dc_chat_is_device_talk(self._dc_chat))
|
||||
|
||||
def is_muted(self) -> bool:
|
||||
"""return true if this chat is muted.
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
"""
|
||||
return bool(lib.dc_chat_is_muted(self._dc_chat))
|
||||
return lib.dc_chat_is_muted(self._dc_chat)
|
||||
|
||||
def is_pinned(self) -> bool:
|
||||
"""Return True if this chat is pinned, False otherwise"""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED
|
||||
|
||||
def is_archived(self) -> bool:
|
||||
"""Return True if this chat is archived, False otherwise.
|
||||
:returns: True if archived, False otherwise
|
||||
"""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
|
||||
def is_contact_request(self) -> bool:
|
||||
"""return True if this chat is a contact request chat.
|
||||
def is_contact_request(self):
|
||||
""" return True if this chat is a contact request chat.
|
||||
|
||||
:returns: True if chat is a contact request chat, False otherwise.
|
||||
"""
|
||||
return bool(lib.dc_chat_is_contact_request(self._dc_chat))
|
||||
return lib.dc_chat_is_contact_request(self._dc_chat)
|
||||
|
||||
def is_promoted(self) -> bool:
|
||||
"""return True if this chat is promoted, i.e.
|
||||
def is_promoted(self):
|
||||
""" return True if this chat is promoted, i.e.
|
||||
the member contacts are aware of their membership,
|
||||
have been sent messages.
|
||||
|
||||
@@ -141,24 +97,24 @@ class Chat(object):
|
||||
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
return bool(lib.dc_chat_can_send(self._dc_chat))
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self) -> bool:
|
||||
"""return True if this chat is a protected chat.
|
||||
""" return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return bool(lib.dc_chat_is_protected(self._dc_chat))
|
||||
return lib.dc_chat_is_protected(self._dc_chat)
|
||||
|
||||
def get_name(self) -> Optional[str]:
|
||||
"""return name of this chat.
|
||||
""" return name of this chat.
|
||||
|
||||
:returns: unicode name
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||
|
||||
def set_name(self, name: str) -> bool:
|
||||
"""set name of this chat.
|
||||
""" set name of this chat.
|
||||
|
||||
:param name: as a unicode string.
|
||||
:returns: True on success, False otherwise
|
||||
@@ -166,20 +122,8 @@ class Chat(object):
|
||||
name = as_dc_charpointer(name)
|
||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
||||
|
||||
def get_color(self):
|
||||
"""return the color of the chat.
|
||||
:returns: color as 0x00rrggbb
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
def get_summary(self):
|
||||
"""return dictionary with summary information."""
|
||||
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
|
||||
s = from_dc_charpointer(dc_res)
|
||||
return json.loads(s)
|
||||
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""mutes the chat
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
:returns: None
|
||||
@@ -193,7 +137,7 @@ class Chat(object):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self) -> None:
|
||||
"""unmutes the chat
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
@@ -201,26 +145,8 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def pin(self) -> None:
|
||||
"""Pin the chat."""
|
||||
lib.dc_set_chat_visibility(self.account._dc_context, self.id, const.DC_CHAT_VISIBILITY_PINNED)
|
||||
|
||||
def unpin(self) -> None:
|
||||
"""Unpin the chat."""
|
||||
if self.is_pinned():
|
||||
lib.dc_set_chat_visibility(self.account._dc_context, self.id, const.DC_CHAT_VISIBILITY_NORMAL)
|
||||
|
||||
def archive(self) -> None:
|
||||
"""Archive the chat."""
|
||||
lib.dc_set_chat_visibility(self.account._dc_context, self.id, const.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
|
||||
def unarchive(self) -> None:
|
||||
"""Unarchive the chat."""
|
||||
if self.is_archived():
|
||||
lib.dc_set_chat_visibility(self.account._dc_context, self.id, const.DC_CHAT_VISIBILITY_NORMAL)
|
||||
|
||||
def get_mute_duration(self) -> int:
|
||||
"""Returns the number of seconds until the mute of this chat is lifted.
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
:param duration:
|
||||
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
|
||||
@@ -228,14 +154,14 @@ class Chat(object):
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self) -> int:
|
||||
"""get ephemeral timer.
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
||||
"""set ephemeral timer.
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
@@ -244,7 +170,7 @@ class Chat(object):
|
||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
||||
|
||||
def get_type(self) -> int:
|
||||
"""(deprecated) return type of this chat.
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
@@ -258,7 +184,7 @@ class Chat(object):
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_join_qr(self) -> Optional[str]:
|
||||
"""get/create Join-Group QR Code as ascii-string.
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||
@@ -294,7 +220,7 @@ class Chat(object):
|
||||
return msg
|
||||
|
||||
def send_text(self, text):
|
||||
"""send a text message and return the resulting Message instance.
|
||||
""" send a text message and return the resulting Message instance.
|
||||
|
||||
:param msg: unicode text
|
||||
:raises ValueError: if message can not be send/chat does not exist.
|
||||
@@ -307,7 +233,7 @@ class Chat(object):
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def send_file(self, path, mime_type="application/octet-stream"):
|
||||
"""send a file and return the resulting Message instance.
|
||||
""" send a file and return the resulting Message instance.
|
||||
|
||||
:param path: path to the file.
|
||||
:param mime_type: the mime-type of this file, defaults to application/octet-stream.
|
||||
@@ -322,7 +248,7 @@ class Chat(object):
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def send_image(self, path):
|
||||
"""send an image message and return the resulting Message instance.
|
||||
""" send an image message and return the resulting Message instance.
|
||||
|
||||
:param path: path to an image file.
|
||||
:raises ValueError: if message can not be send/chat does not exist.
|
||||
@@ -337,7 +263,7 @@ class Chat(object):
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def prepare_message(self, msg):
|
||||
"""prepare a message for sending.
|
||||
""" prepare a message for sending.
|
||||
|
||||
:param msg: the message to be prepared.
|
||||
:returns: a :class:`deltachat.message.Message` instance.
|
||||
@@ -352,7 +278,7 @@ class Chat(object):
|
||||
return msg
|
||||
|
||||
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
||||
"""prepare a message for sending and return the resulting Message instance.
|
||||
""" prepare a message for sending and return the resulting Message instance.
|
||||
|
||||
To actually send the message, call :meth:`send_prepared`.
|
||||
The file must be inside the blob directory.
|
||||
@@ -368,7 +294,7 @@ class Chat(object):
|
||||
return self.prepare_message(msg)
|
||||
|
||||
def send_prepared(self, message):
|
||||
"""send a previously prepared message.
|
||||
""" send a previously prepared message.
|
||||
|
||||
:param message: a :class:`Message` instance previously returned by
|
||||
:meth:`prepare_file`.
|
||||
@@ -388,7 +314,7 @@ class Chat(object):
|
||||
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
|
||||
|
||||
def set_draft(self, message):
|
||||
"""set message as draft.
|
||||
""" set message as draft.
|
||||
|
||||
:param message: a :class:`Message` instance
|
||||
:returns: None
|
||||
@@ -399,7 +325,7 @@ class Chat(object):
|
||||
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
|
||||
|
||||
def get_draft(self):
|
||||
"""get draft message for this chat.
|
||||
""" get draft message for this chat.
|
||||
|
||||
:param message: a :class:`Message` instance
|
||||
:returns: Message object or None (if no draft available)
|
||||
@@ -411,34 +337,40 @@ class Chat(object):
|
||||
return Message(self.account, dc_msg)
|
||||
|
||||
def get_messages(self):
|
||||
"""return list of messages in this chat.
|
||||
""" return list of messages in this chat.
|
||||
|
||||
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
||||
"""
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
|
||||
safe_unref(lib.dc_array_unref),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
||||
|
||||
def count_fresh_messages(self):
|
||||
"""return number of fresh messages in this chat.
|
||||
""" return number of fresh messages in this chat.
|
||||
|
||||
:returns: number of fresh messages
|
||||
"""
|
||||
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
|
||||
|
||||
def mark_noticed(self):
|
||||
"""mark all messages in this chat as noticed.
|
||||
""" mark all messages in this chat as noticed.
|
||||
|
||||
Noticed messages are no longer fresh.
|
||||
"""
|
||||
return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
|
||||
|
||||
def get_summary(self):
|
||||
""" return dictionary with summary information. """
|
||||
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
|
||||
s = from_dc_charpointer(dc_res)
|
||||
return json.loads(s)
|
||||
|
||||
# ------ group management API ------------------------------
|
||||
|
||||
def add_contact(self, obj):
|
||||
"""add a contact to this chat.
|
||||
""" add a contact to this chat.
|
||||
|
||||
:params obj: Contact, Account or e-mail address.
|
||||
:raises ValueError: if contact could not be added
|
||||
@@ -451,7 +383,7 @@ class Chat(object):
|
||||
return contact
|
||||
|
||||
def remove_contact(self, obj):
|
||||
"""remove a contact from this chat.
|
||||
""" remove a contact from this chat.
|
||||
|
||||
:params obj: Contact, Account or e-mail address.
|
||||
:raises ValueError: if contact could not be removed
|
||||
@@ -463,22 +395,23 @@ class Chat(object):
|
||||
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||
|
||||
def get_contacts(self):
|
||||
"""get all contacts for this chat.
|
||||
""" get all contacts for this chat.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
||||
safe_unref(lib.dc_array_unref),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(
|
||||
dc_array, lambda id: Contact(self.account, id))
|
||||
)
|
||||
return list(iter_array(dc_array, lambda id: Contact(self.account, id)))
|
||||
|
||||
def num_contacts(self):
|
||||
"""return number of contacts in this chat."""
|
||||
""" return number of contacts in this chat. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
||||
safe_unref(lib.dc_array_unref),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return lib.dc_array_get_cnt(dc_array)
|
||||
|
||||
@@ -525,6 +458,12 @@ class Chat(object):
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
def get_color(self):
|
||||
"""return the color of the chat.
|
||||
:returns: color as 0x00rrggbb
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
# ------ location streaming API ------------------------------
|
||||
|
||||
def is_sending_locations(self):
|
||||
@@ -533,6 +472,12 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
|
||||
|
||||
def is_archived(self):
|
||||
"""return True if this chat is archived.
|
||||
:returns: True if archived.
|
||||
"""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
|
||||
def enable_sending_locations(self, seconds):
|
||||
"""enable sending locations for this chat.
|
||||
|
||||
@@ -568,7 +513,10 @@ class Chat(object):
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||
timestamp=datetime.fromtimestamp(lib.dc_array_get_timestamp(dc_array, i), timezone.utc),
|
||||
timestamp=datetime.fromtimestamp(
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
),
|
||||
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
||||
|
||||
@@ -10,21 +10,17 @@ from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
"""Delta-Chat Contact.
|
||||
""" Delta-Chat Contact.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
from .account import Account
|
||||
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
def __eq__(self, other):
|
||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
@@ -35,16 +31,19 @@ class Contact(object):
|
||||
|
||||
@property
|
||||
def _dc_contact(self):
|
||||
return ffi.gc(lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_contact_unref)
|
||||
return ffi.gc(
|
||||
lib.dc_get_contact(self.account._dc_context, self.id),
|
||||
lib.dc_contact_unref
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self) -> str:
|
||||
"""normalized e-mail address for this account."""
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self) -> str:
|
||||
"""display name for this contact."""
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
@@ -53,26 +52,28 @@ class Contact(object):
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc)
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
"""Return True if the contact is blocked."""
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
|
||||
def set_blocked(self, block=True):
|
||||
"""[Deprecated, use block/unblock methods] Block or unblock a contact."""
|
||||
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, block)
|
||||
|
||||
def block(self):
|
||||
"""Block this contact. Message will not be seen/retrieved from this contact."""
|
||||
""" Block this contact. Message will not be seen/retrieved from this contact. """
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, True)
|
||||
|
||||
def unblock(self):
|
||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self):
|
||||
"""Return True if the contact is verified."""
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
@@ -92,7 +93,7 @@ class Contact(object):
|
||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
||||
|
||||
def create_chat(self):
|
||||
"""create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
:param contact: chat_id (int) or contact object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, Generator, Optional, TypeVar
|
||||
from typing import Optional, TypeVar, Generator, Callable
|
||||
|
||||
from .capi import ffi, lib
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def safe_unref(unref: Callable) -> Callable:
|
||||
def wrapper(obj) -> None:
|
||||
if obj != ffi.NULL:
|
||||
unref(obj)
|
||||
|
||||
return wrapper
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
|
||||
@@ -3,27 +3,18 @@ Internal Python-level IMAP handling used by the testplugin
|
||||
and for cleaning up inbox/mvbox for each test function run.
|
||||
"""
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import pathlib
|
||||
import ssl
|
||||
import pathlib
|
||||
from contextlib import contextmanager
|
||||
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
|
||||
import imaplib
|
||||
from deltachat import const, Account
|
||||
from typing import List
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from deltachat import Account, const
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
FLAGS = b'FLAGS'
|
||||
FETCH = b'FETCH'
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
@@ -78,8 +69,8 @@ class DirectImap:
|
||||
return self.conn.folder.set(foldername)
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None."""
|
||||
""" Return info about selected folder if it is
|
||||
configured, otherwise None. """
|
||||
if "_" not in config_name:
|
||||
config_name = "configured_{}_folder".format(config_name)
|
||||
foldername = self.account.get_config(config_name)
|
||||
@@ -87,17 +78,17 @@ class DirectImap:
|
||||
return self.select_folder(foldername)
|
||||
|
||||
def list_folders(self) -> List[str]:
|
||||
"""return list of all existing folder names"""
|
||||
""" return list of all existing folder names"""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
"""delete a range of messages (imap-syntax).
|
||||
""" delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)')
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
@@ -150,13 +141,7 @@ class DirectImap:
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log(
|
||||
"Message",
|
||||
msg.uid,
|
||||
msg.flags,
|
||||
"Message-Id:",
|
||||
msg.obj.get("Message-Id"),
|
||||
)
|
||||
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id"))
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
@@ -165,7 +150,7 @@ class DirectImap:
|
||||
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
"""return Idle ContextManager."""
|
||||
""" return Idle ContextManager. """
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
@@ -178,11 +163,11 @@ class DirectImap:
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding='ascii'), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
@@ -198,7 +183,7 @@ class IdleManager:
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> List[bytes]:
|
||||
"""(blocking) wait for next idle message from server."""
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
@@ -207,19 +192,20 @@ class IdleManager:
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b"EXISTS" in item or b"RECENT" in item:
|
||||
if b'EXISTS' in item or b'RECENT' in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
"""Return first message with SEEN flag from a running idle-stream."""
|
||||
""" Return first message with SEEN flag from a running idle-stream.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
if FLAGS in item and rb'\Seen' in item:
|
||||
return int(item.split(b' ')[1])
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
res = self.direct_imap.conn.idle.stop()
|
||||
return res
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import traceback
|
||||
from contextlib import contextmanager
|
||||
from queue import Empty, Queue
|
||||
import time
|
||||
import io
|
||||
import re
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
|
||||
import deltachat
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer, safe_unref
|
||||
from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
@@ -27,10 +26,9 @@ class FFIEvent:
|
||||
|
||||
|
||||
class FFIEventLogger:
|
||||
"""If you register an instance of this logger with an Account
|
||||
""" If you register an instance of this logger with an Account
|
||||
you'll get all ffi-events printed.
|
||||
"""
|
||||
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
@@ -58,9 +56,9 @@ class FFIEventLogger:
|
||||
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||
|
||||
if os.name == "posix":
|
||||
WARN = "\033[93m"
|
||||
ERROR = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
WARN = '\033[93m'
|
||||
ERROR = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
if message.startswith("DC_EVENT_WARNING"):
|
||||
s = WARN + s + ENDC
|
||||
if message.startswith("DC_EVENT_ERROR"):
|
||||
@@ -173,12 +171,12 @@ class FFIEventTracker:
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
"""wait for and return next incoming message."""
|
||||
""" wait for and return next incoming message. """
|
||||
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
|
||||
def wait_next_messages_changed(self):
|
||||
"""wait for and return next message-changed message or None
|
||||
""" wait for and return next message-changed message or None
|
||||
if the event contains no msgid"""
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
@@ -193,11 +191,10 @@ class FFIEventTracker:
|
||||
|
||||
|
||||
class EventThread(threading.Thread):
|
||||
"""Event Thread for an account.
|
||||
""" Event Thread for an account.
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
@@ -222,14 +219,14 @@ class EventThread(threading.Thread):
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self) -> None:
|
||||
"""get and run events until shutdown."""
|
||||
""" get and run events until shutdown. """
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
self._inner_run()
|
||||
|
||||
def _inner_run(self):
|
||||
event_emitter = ffi.gc(
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
safe_unref(lib.dc_event_emitter_unref),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
@@ -262,7 +259,8 @@ class EventThread(threading.Thread):
|
||||
except Exception as ex:
|
||||
logfile = io.StringIO()
|
||||
traceback.print_exception(*sys.exc_info(), file=logfile)
|
||||
self.account.log("{}\nException {}\nTraceback:\n{}".format(info, ex, logfile.getvalue()))
|
||||
self.account.log("{}\nException {}\nTraceback:\n{}"
|
||||
.format(info, ex, logfile.getvalue()))
|
||||
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
@@ -284,10 +282,7 @@ class EventThread(threading.Thread):
|
||||
yield res
|
||||
yield "ac_outgoing_message", dict(message=msg)
|
||||
elif msg.is_in_fresh():
|
||||
yield map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
dict(message=msg),
|
||||
)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", 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)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import pluggy
|
||||
|
||||
|
||||
account_spec_name = "deltachat-account"
|
||||
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
||||
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
||||
@@ -12,13 +13,12 @@ global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
||||
|
||||
|
||||
class PerAccount:
|
||||
"""per-Account-instance hook specifications.
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
All hooks are executed in a dedicated Event thread.
|
||||
Hooks are generally not allowed to block/last long as this
|
||||
blocks overall event processing on the python side.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _make_plugin_manager(cls):
|
||||
pm = pluggy.PluginManager(account_spec_name)
|
||||
@@ -27,7 +27,7 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
"""process a CFFI low level events for a given account.
|
||||
""" process a CFFI low level events for a given account.
|
||||
|
||||
ffi_event has "name", "data1", "data2" values as specified
|
||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||
@@ -35,37 +35,37 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_log_line(self, message):
|
||||
"""log a message related to the account."""
|
||||
""" log a message related to the account. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_configure_completed(self, success):
|
||||
"""Called after a configure process completed."""
|
||||
""" Called after a configure process completed. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_incoming_message(self, message):
|
||||
"""Called on any incoming message (both existing chats and contact requests)."""
|
||||
""" Called on any incoming message (both existing chats and contact requests). """
|
||||
|
||||
@account_hookspec
|
||||
def ac_outgoing_message(self, message):
|
||||
"""Called on each outgoing message (both system and "normal")."""
|
||||
""" Called on each outgoing message (both system and "normal")."""
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
"""Called when an outgoing message has been delivered to SMTP.
|
||||
""" Called when an outgoing message has been delivered to SMTP.
|
||||
|
||||
:param message: Message that was just delivered.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
"""Chat was created or modified regarding membership, avatar, title.
|
||||
""" Chat was created or modified regarding membership, avatar, title.
|
||||
|
||||
:param chat: Chat which was modified.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
"""Called for each contact added to an accepted chat.
|
||||
""" Called for each contact added to an accepted chat.
|
||||
|
||||
:param chat: Chat where contact was added.
|
||||
:param contact: Contact that was added.
|
||||
@@ -75,7 +75,7 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
"""Called for each contact removed from a chat.
|
||||
""" Called for each contact removed from a chat.
|
||||
|
||||
:param chat: Chat where contact was removed.
|
||||
:param contact: Contact that was removed.
|
||||
@@ -85,11 +85,10 @@ class PerAccount:
|
||||
|
||||
|
||||
class Global:
|
||||
"""global hook specifications using a per-process singleton
|
||||
""" global hook specifications using a per-process singleton
|
||||
plugin manager instance.
|
||||
|
||||
"""
|
||||
|
||||
_plugin_manager = None
|
||||
|
||||
@classmethod
|
||||
@@ -101,11 +100,11 @@ class Global:
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_init(self, account):
|
||||
"""called when `Account::__init__()` function starts executing."""
|
||||
""" called when `Account::__init__()` function starts executing. """
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_extra_configure(self, account):
|
||||
"""Called when account configuration successfully finished.
|
||||
""" Called when account configuration successfully finished.
|
||||
|
||||
This hook can be used to perform extra work before
|
||||
ac_configure_completed is called.
|
||||
@@ -113,4 +112,4 @@ class Global:
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_after_shutdown(self, account):
|
||||
"""Called after the account has been shutdown."""
|
||||
""" Called after the account has been shutdown. """
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
""" The Message object. """
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime, timezone
|
||||
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,
|
||||
safe_unref,
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
"""Message object.
|
||||
""" Message object.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account` or
|
||||
:class:`deltachat.chat.Chat`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, dc_msg):
|
||||
self.account = account
|
||||
assert isinstance(self.account._dc_context, ffi.CData)
|
||||
@@ -32,48 +25,42 @@ class Message(object):
|
||||
msg_id = self.id
|
||||
assert msg_id is not None and msg_id >= 0, repr(msg_id)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
def __eq__(self, other):
|
||||
return self.account == other.account and self.id == other.id
|
||||
|
||||
def __repr__(self):
|
||||
c = self.get_sender_contact()
|
||||
typ = "outgoing" if self.is_outgoing() else "incoming"
|
||||
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
|
||||
typ,
|
||||
self.is_system_message(),
|
||||
repr(self.text[:10]),
|
||||
self.id,
|
||||
c.id,
|
||||
c.addr,
|
||||
self.chat.id,
|
||||
self.chat.get_name(),
|
||||
)
|
||||
typ, self.is_system_message(), repr(self.text[:10]),
|
||||
self.id, c.id, c.addr, self.chat.id, self.chat.get_name())
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, account, id):
|
||||
assert id > 0
|
||||
return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), safe_unref(lib.dc_msg_unref)))
|
||||
return cls(account, ffi.gc(
|
||||
lib.dc_get_msg(account._dc_context, id),
|
||||
lib.dc_msg_unref
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def new_empty(cls, account, view_type):
|
||||
"""create a non-persistent message.
|
||||
""" create a non-persistent message.
|
||||
|
||||
:param view_type: the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker", "videochat", "webxdc"
|
||||
:param: view_type is the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker"
|
||||
"""
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
else:
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
return Message(
|
||||
account,
|
||||
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), safe_unref(lib.dc_msg_unref)),
|
||||
)
|
||||
return Message(account, ffi.gc(
|
||||
lib.dc_msg_new(account._dc_context, view_type_code),
|
||||
lib.dc_msg_unref
|
||||
))
|
||||
|
||||
def create_chat(self):
|
||||
"""create or get an existing chat (group) object for this message.
|
||||
""" create or get an existing chat (group) object for this message.
|
||||
|
||||
If the message is a contact request
|
||||
the sender will become an accepted contact.
|
||||
@@ -85,22 +72,23 @@ class Message(object):
|
||||
|
||||
@props.with_doc
|
||||
def id(self):
|
||||
"""id of this message."""
|
||||
"""id of this message. """
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message)."""
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
def set_text(self, text):
|
||||
"""set text of this message."""
|
||||
"""set text of this message. """
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message)."""
|
||||
return from_optional_dc_charpointer(lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
"""return True if this message has an html part, False otherwise."""
|
||||
@@ -115,11 +103,11 @@ class Message(object):
|
||||
|
||||
@props.with_doc
|
||||
def filename(self):
|
||||
"""filename if there was an attachment, otherwise empty string."""
|
||||
"""filename if there was an attachment, otherwise empty string. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
||||
|
||||
def set_file(self, path, mime_type=None):
|
||||
"""set file for this message from path and mime_type."""
|
||||
"""set file for this message from path and mime_type. """
|
||||
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
|
||||
if not os.path.exists(path):
|
||||
raise ValueError("path does not exist: {!r}".format(path))
|
||||
@@ -127,7 +115,7 @@ class Message(object):
|
||||
|
||||
@props.with_doc
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string."""
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@@ -136,74 +124,44 @@ class Message(object):
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def get_status_updates(self, serial: int = 0) -> list:
|
||||
"""Get the status updates of this webxdc message.
|
||||
|
||||
The status updates may be sent by yourself or by other members.
|
||||
If this message doesn't have a webxdc instance, an empty list is returned.
|
||||
|
||||
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
|
||||
"""
|
||||
return json.loads(
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial))
|
||||
)
|
||||
|
||||
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
|
||||
"""Send an status update for the webxdc instance of this message.
|
||||
|
||||
If the webxdc instance is a draft, the update is not sent immediately.
|
||||
Instead, the updates are collected and sent out in a batch when the instance is actually sent.
|
||||
|
||||
:param json_data: program-readable data, the actual payload.
|
||||
:param description: The user-visible description of JSON data
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
if isinstance(json_data, dict):
|
||||
json_data = json.dumps(json_data, default=str)
|
||||
return bool(
|
||||
lib.dc_send_webxdc_status_update(
|
||||
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description)
|
||||
)
|
||||
)
|
||||
|
||||
def is_system_message(self):
|
||||
"""return True if this message is a system/info message."""
|
||||
""" return True if this message is a system/info message. """
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
|
||||
def is_setup_message(self):
|
||||
"""return True if this message is a setup message."""
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self) -> str:
|
||||
"""return the first characters of a setup code in a setup message."""
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
def is_encrypted(self):
|
||||
"""return True if this message was encrypted."""
|
||||
""" return True if this message was encrypted. """
|
||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
||||
|
||||
def is_bot(self):
|
||||
"""return True if this message is submitted automatically."""
|
||||
""" return True if this message is submitted automatically. """
|
||||
return bool(lib.dc_msg_is_bot(self._dc_msg))
|
||||
|
||||
def is_forwarded(self):
|
||||
"""return True if this message was forwarded."""
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self) -> str:
|
||||
"""Return informational text for a single message.
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id))
|
||||
|
||||
def get_summarytext(self, width: int) -> str:
|
||||
"""Get a message summary as a single line of text. Typically used for notifications."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
|
||||
|
||||
def continue_key_transfer(self, setup_code):
|
||||
"""extract key and use it as primary key for this account."""
|
||||
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||
""" extract key and use it as primary key for this account. """
|
||||
res = lib.dc_continue_key_transfer(
|
||||
self.account._dc_context,
|
||||
self.id,
|
||||
as_dc_charpointer(setup_code)
|
||||
)
|
||||
if res == 0:
|
||||
raise ValueError("could not decrypt")
|
||||
|
||||
@@ -272,7 +230,7 @@ class Message(object):
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
"""return mime-header object for an incoming message.
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
This only returns a non-None object if ``save_mime_headers``
|
||||
config option was set and the message is incoming.
|
||||
@@ -280,10 +238,9 @@ class Message(object):
|
||||
:returns: email-mime message object (with headers only, no body).
|
||||
"""
|
||||
import email.parser
|
||||
|
||||
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||
if mime_headers:
|
||||
s = ffi.string(ffi.gc(mime_headers, safe_unref(lib.dc_str_unref)))
|
||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
||||
if isinstance(s, bytes):
|
||||
return email.message_from_bytes(s)
|
||||
return email.message_from_string(s)
|
||||
@@ -300,7 +257,6 @@ class Message(object):
|
||||
:returns: :class:`deltachat.chat.Chat` object
|
||||
"""
|
||||
from .chat import Chat
|
||||
|
||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@@ -310,11 +266,13 @@ class Message(object):
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
"""set different sender name for a message."""
|
||||
lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name))
|
||||
"""set different sender name for a message. """
|
||||
lib.dc_msg_set_override_sender_name(
|
||||
self._dc_msg, as_dc_charpointer(name))
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
@@ -329,7 +287,6 @@ class Message(object):
|
||||
:returns: :class:`deltachat.chat.Contact` instance
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self.account, contact_id)
|
||||
|
||||
@@ -342,11 +299,14 @@ class Message(object):
|
||||
dc_msg = self._dc_msg
|
||||
else:
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), safe_unref(lib.dc_msg_unref))
|
||||
dc_msg = ffi.gc(
|
||||
lib.dc_get_msg(self.account._dc_context, self.id),
|
||||
lib.dc_msg_unref
|
||||
)
|
||||
return lib.dc_msg_get_state(dc_msg)
|
||||
|
||||
def is_in_fresh(self):
|
||||
"""return True if Message is incoming fresh message (un-noticed).
|
||||
""" return True if Message is incoming fresh message (un-noticed).
|
||||
|
||||
Fresh messages are not noticed nor seen and are typically
|
||||
shown in notifications.
|
||||
@@ -370,25 +330,25 @@ class Message(object):
|
||||
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
"""Return True if Message is outgoing. """
|
||||
return self._msgstate in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
const.DC_STATE_OUT_MDN_RCVD,
|
||||
const.DC_STATE_OUT_DELIVERED,
|
||||
)
|
||||
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
|
||||
const.DC_STATE_OUT_DELIVERED)
|
||||
|
||||
def is_out_preparing(self):
|
||||
"""Return True if Message is outgoing, but its file is being prepared."""
|
||||
"""Return True if Message is outgoing, but its file is being prepared.
|
||||
"""
|
||||
return self._msgstate == const.DC_STATE_OUT_PREPARING
|
||||
|
||||
def is_out_pending(self):
|
||||
"""Return True if Message is outgoing, but is pending (no single checkmark)."""
|
||||
"""Return True if Message is outgoing, but is pending (no single checkmark).
|
||||
"""
|
||||
return self._msgstate == const.DC_STATE_OUT_PENDING
|
||||
|
||||
def is_out_failed(self):
|
||||
"""Return True if Message is unrecoverably failed."""
|
||||
"""Return True if Message is unrecoverably failed.
|
||||
"""
|
||||
return self._msgstate == const.DC_STATE_OUT_FAILED
|
||||
|
||||
def is_out_delivered(self):
|
||||
@@ -415,58 +375,48 @@ class Message(object):
|
||||
return lib.dc_msg_get_viewtype(self._dc_msg)
|
||||
|
||||
def is_text(self):
|
||||
"""return True if it's a text message."""
|
||||
""" return True if it's a text message. """
|
||||
return self._view_type == const.DC_MSG_TEXT
|
||||
|
||||
def is_image(self):
|
||||
"""return True if it's an image message."""
|
||||
""" return True if it's an image message. """
|
||||
return self._view_type == const.DC_MSG_IMAGE
|
||||
|
||||
def is_gif(self):
|
||||
"""return True if it's a gif message."""
|
||||
""" return True if it's a gif message. """
|
||||
return self._view_type == const.DC_MSG_GIF
|
||||
|
||||
def is_sticker(self):
|
||||
"""return True if it's a sticker message."""
|
||||
""" return True if it's a sticker message. """
|
||||
return self._view_type == const.DC_MSG_STICKER
|
||||
|
||||
def is_audio(self):
|
||||
"""return True if it's an audio message."""
|
||||
""" return True if it's an audio message. """
|
||||
return self._view_type == const.DC_MSG_AUDIO
|
||||
|
||||
def is_video(self):
|
||||
"""return True if it's a video message."""
|
||||
""" return True if it's a video message. """
|
||||
return self._view_type == const.DC_MSG_VIDEO
|
||||
|
||||
def is_videochat_invitation(self):
|
||||
"""return True if it's a videochat invitation message."""
|
||||
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
|
||||
|
||||
def is_webxdc(self):
|
||||
"""return True if it's a Webxdc message."""
|
||||
return self._view_type == const.DC_MSG_WEBXDC
|
||||
|
||||
def is_file(self):
|
||||
"""return True if it's a file message."""
|
||||
""" return True if it's a file message. """
|
||||
return self._view_type == const.DC_MSG_FILE
|
||||
|
||||
def mark_seen(self):
|
||||
"""mark this message as seen."""
|
||||
""" mark this message as seen. """
|
||||
self.account.mark_seen_messages([self.id])
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
_view_type_mapping = {
|
||||
"text": const.DC_MSG_TEXT,
|
||||
"image": const.DC_MSG_IMAGE,
|
||||
"gif": const.DC_MSG_GIF,
|
||||
"audio": const.DC_MSG_AUDIO,
|
||||
"video": const.DC_MSG_VIDEO,
|
||||
"file": const.DC_MSG_FILE,
|
||||
"sticker": const.DC_MSG_STICKER,
|
||||
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
|
||||
"webxdc": const.DC_MSG_WEBXDC,
|
||||
'text': const.DC_MSG_TEXT,
|
||||
'image': const.DC_MSG_IMAGE,
|
||||
'gif': const.DC_MSG_GIF,
|
||||
'audio': const.DC_MSG_AUDIO,
|
||||
'video': const.DC_MSG_VIDEO,
|
||||
'file': const.DC_MSG_FILE,
|
||||
'sticker': const.DC_MSG_STICKER,
|
||||
}
|
||||
|
||||
|
||||
@@ -474,16 +424,14 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
code = _view_type_mapping.get(view_type_name)
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, " "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))
|
||||
)
|
||||
raise ValueError("message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
|
||||
|
||||
|
||||
#
|
||||
# some helper code for turning system messages into hook events
|
||||
#
|
||||
|
||||
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
@@ -500,7 +448,7 @@ def map_system_message(msg):
|
||||
|
||||
|
||||
def extract_addr(text):
|
||||
m = re.match(r".*\((.+@.+)\)", text)
|
||||
m = re.match(r'.*\((.+@.+)\)', text)
|
||||
if m:
|
||||
text = m.group(1)
|
||||
text = text.rstrip(".")
|
||||
@@ -508,9 +456,9 @@ def extract_addr(text):
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
""" return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple"""
|
||||
returns a (action, affected, actor) triple """
|
||||
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
@@ -519,7 +467,7 @@ def parse_system_add_remove(text):
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
text = text.lower()
|
||||
m = re.match(r"member (.+) (removed|added) by (.+)", text)
|
||||
m = re.match(r'member (.+) (removed|added) by (.+)', text)
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
|
||||
@@ -9,7 +9,6 @@ def with_doc(f):
|
||||
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
|
||||
def cached(f):
|
||||
"""returns a cached property that is calculated by function f"""
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
return self._property_cache[f]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Provider info class."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, safe_unref
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer
|
||||
|
||||
|
||||
class ProviderNotFoundError(Exception):
|
||||
@@ -17,7 +17,7 @@ class Provider(object):
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
safe_unref(lib.dc_provider_unref),
|
||||
lib.dc_provider_unref,
|
||||
)
|
||||
if provider == ffi.NULL:
|
||||
raise ProviderNotFoundError("Provider not found")
|
||||
@@ -26,12 +26,14 @@ class Provider(object):
|
||||
@property
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self) -> int:
|
||||
|
||||
@@ -1,62 +1,56 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import fnmatch
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import io
|
||||
import subprocess
|
||||
import queue
|
||||
import threading
|
||||
import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
from queue import Queue
|
||||
from typing import Callable, List, Optional
|
||||
from typing import List, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import pathlib
|
||||
|
||||
from . import Account, const, account_hookimpl, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
from _pytest._code import Source
|
||||
|
||||
import deltachat
|
||||
|
||||
from . import Account, account_hookimpl, const, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
group.addoption(
|
||||
"--liveconfig",
|
||||
action="store",
|
||||
default=None,
|
||||
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
group.addoption(
|
||||
"--ignored",
|
||||
action="store_true",
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
group.addoption(
|
||||
"--strict-tls",
|
||||
action="store_true",
|
||||
"--strict-tls", action="store_true",
|
||||
help="Never accept invalid TLS certificates for test accounts",
|
||||
)
|
||||
group.addoption(
|
||||
"--extra-info",
|
||||
action="store_true",
|
||||
help="show more info on failures (imap server state, config)",
|
||||
"--extra-info", action="store_true",
|
||||
help="show more info on failures (imap server state, config)"
|
||||
)
|
||||
group.addoption(
|
||||
"--debug-setup",
|
||||
action="store_true",
|
||||
help="show events during configure and start io phases of online accounts",
|
||||
"--debug-setup", action="store_true",
|
||||
help="show events during configure and start io phases of online accounts"
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
cfg = config.getoption("--liveconfig")
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
@@ -119,21 +113,19 @@ def pytest_configure(config):
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
info = get_core_info()
|
||||
summary = [
|
||||
"Deltachat core={} sqlite={} journal_mode={}".format(
|
||||
info["deltachat_core_version"],
|
||||
info["sqlite_version"],
|
||||
info["journal_mode"],
|
||||
)
|
||||
]
|
||||
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
info['journal_mode'],
|
||||
)]
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "?" in cfg:
|
||||
url, token = cfg.split("?", 1)
|
||||
summary.append("Liveconfig provider: {}?<token ommitted>".format(url))
|
||||
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
|
||||
else:
|
||||
summary.append("Liveconfig file: {}".format(cfg))
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
|
||||
|
||||
@@ -143,15 +135,15 @@ def testprocess(request):
|
||||
|
||||
|
||||
class TestProcess:
|
||||
"""A pytest session-scoped instance to help with managing "live" account configurations."""
|
||||
|
||||
""" A pytest session-scoped instance to help with managing "live" account configurations.
|
||||
"""
|
||||
def __init__(self, pytestconfig):
|
||||
self.pytestconfig = pytestconfig
|
||||
self._addr2files = {}
|
||||
self._configlist = []
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
"""provide live account configs, cached on a per-test-process scope
|
||||
""" provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can re-use already known live configs.
|
||||
Depending on the --liveconfig option this comes from
|
||||
a HTTP provider or a file with a line specifying each accounts config.
|
||||
@@ -162,7 +154,7 @@ class TestProcess:
|
||||
|
||||
if not liveconfig_opt.startswith("http"):
|
||||
for line in open(liveconfig_opt):
|
||||
if line.strip() and not line.strip().startswith("#"):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
@@ -178,7 +170,8 @@ class TestProcess:
|
||||
except IndexError:
|
||||
res = requests.post(liveconfig_opt)
|
||||
if res.status_code != 200:
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(
|
||||
index, res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
@@ -237,17 +230,13 @@ def data(request):
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
# users like "deltabot".
|
||||
self.paths = [
|
||||
os.path.normpath(x)
|
||||
for x in [
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "..", "..", "test-data"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data"),
|
||||
]
|
||||
]
|
||||
self.paths = [os.path.normpath(x) for x in [
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
|
||||
]]
|
||||
|
||||
def get_path(self, bn):
|
||||
"""return path of file or None if it doesn't exist."""
|
||||
""" return path of file or None if it doesn't exist. """
|
||||
for path in self.paths:
|
||||
fn = os.path.join(path, *bn.split("/"))
|
||||
if os.path.exists(fn):
|
||||
@@ -264,11 +253,10 @@ def data(request):
|
||||
|
||||
|
||||
class ACSetup:
|
||||
"""accounts setup helper to deal with multiple configure-process
|
||||
""" accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases. From tests, use the higher level
|
||||
public ACFactory methods instead of its private helper class.
|
||||
"""
|
||||
|
||||
CONFIGURING = "CONFIGURING"
|
||||
CONFIGURED = "CONFIGURED"
|
||||
IDLEREADY = "IDLEREADY"
|
||||
@@ -284,14 +272,13 @@ class ACSetup:
|
||||
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
|
||||
|
||||
def add_configured(self, account):
|
||||
"""add an already configured account."""
|
||||
""" add an already configured account. """
|
||||
assert account.is_configured()
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
"""add an account and start its configure process."""
|
||||
|
||||
""" add an account and start its configure process. """
|
||||
class PendingTracker:
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(this, success):
|
||||
@@ -303,7 +290,7 @@ class ACSetup:
|
||||
self.log("started configure on", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
"""wait until this account has successfully configured."""
|
||||
""" wait until this account has successfully configured. """
|
||||
if self._account2state[account] == self.CONFIGURING:
|
||||
while 1:
|
||||
acc = self._pop_config_success()
|
||||
@@ -314,7 +301,7 @@ class ACSetup:
|
||||
acc._evtracker.consume_events()
|
||||
|
||||
def bring_online(self):
|
||||
"""Wait for all accounts to become ready to receive messages.
|
||||
""" Wait for all accounts to become ready to receive messages.
|
||||
|
||||
This will initialize logging, start IO and the direct_imap attribute
|
||||
for each account which either is CONFIGURED already or which is CONFIGURING
|
||||
@@ -349,12 +336,12 @@ class ACSetup:
|
||||
acc.log("inbox IDLE ready")
|
||||
|
||||
def init_logging(self, acc):
|
||||
"""idempotent function for initializing logging (will replace existing logger)."""
|
||||
""" idempotent function for initializing logging (will replace existing logger). """
|
||||
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
|
||||
acc.add_account_plugin(logger, name="logger-" + acc._logid)
|
||||
|
||||
def init_imap(self, acc):
|
||||
"""initialize direct_imap and cleanup server state."""
|
||||
""" initialize direct_imap and cleanup server state. """
|
||||
from deltachat.direct_imap import DirectImap
|
||||
|
||||
assert acc.is_configured()
|
||||
@@ -388,7 +375,8 @@ class ACFactory:
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self._acsetup = ACSetup(testprocess, self.init_time)
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"]
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
request.addfinalizer(self.finalize)
|
||||
|
||||
@@ -411,7 +399,7 @@ class ACFactory:
|
||||
acc.disable_logging()
|
||||
|
||||
def get_next_liveconfig(self):
|
||||
"""Base function to get functional online configurations
|
||||
""" Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
@@ -430,16 +418,16 @@ class ACFactory:
|
||||
if addr in self.testprocess._addr2files:
|
||||
return self._getaccount(addr)
|
||||
|
||||
def get_unconfigured_account(self, closed=False):
|
||||
return self._getaccount(closed=closed)
|
||||
def get_unconfigured_account(self):
|
||||
return self._getaccount()
|
||||
|
||||
def _getaccount(self, try_cache_addr=None, closed=False):
|
||||
def _getaccount(self, try_cache_addr=None):
|
||||
logid = "ac{}".format(len(self._accounts) + 1)
|
||||
# we need to use fixed database basename for maybe_cache_* functions to work
|
||||
path = self.tmpdir.mkdir(logid).join("dc.db")
|
||||
if try_cache_addr:
|
||||
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
|
||||
ac = Account(path.strpath, logging=self._logging, closed=closed)
|
||||
ac = Account(path.strpath, logging=self._logging)
|
||||
ac._logid = logid # later instantiated FFIEventLogger needs this
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
if self.pytestconfig.getoption("--debug-setup"):
|
||||
@@ -468,23 +456,16 @@ class ACFactory:
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
|
||||
def get_pseudo_configured_account(self):
|
||||
# do a pseudo-configured account
|
||||
ac = self.get_unconfigured_account(closed=bool(passphrase))
|
||||
if passphrase:
|
||||
ac.open(passphrase)
|
||||
ac = self.get_unconfigured_account()
|
||||
acname = ac._logid
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(
|
||||
dict(
|
||||
addr=addr,
|
||||
displayname=acname,
|
||||
mail_pw="123",
|
||||
configured_addr=addr,
|
||||
configured_mail_pw="123",
|
||||
configured="1",
|
||||
)
|
||||
)
|
||||
ac.update_config(dict(
|
||||
addr=addr, displayname=acname, mail_pw="123",
|
||||
configured_addr=addr, configured_mail_pw="123",
|
||||
configured="1",
|
||||
))
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
@@ -520,7 +501,7 @@ class ACFactory:
|
||||
return ac
|
||||
|
||||
def wait_configured(self, account):
|
||||
"""Wait until the specified account has successfully completed configure."""
|
||||
""" Wait until the specified account has successfully completed configure. """
|
||||
self._acsetup.wait_one_configured(account)
|
||||
|
||||
def bring_accounts_online(self):
|
||||
@@ -550,10 +531,8 @@ class ACFactory:
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email",
|
||||
bot_cfg["addr"],
|
||||
"--password",
|
||||
bot_cfg["mail_pw"],
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
@@ -564,9 +543,9 @@ class ACFactory:
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True, # give back text
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True # give back text
|
||||
)
|
||||
bot = BotProcess(popen, addr=bot_cfg["addr"])
|
||||
self._finalizers.append(bot.kill)
|
||||
@@ -586,7 +565,7 @@ class ACFactory:
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1 :]:
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
|
||||
from .hookspec import Global, account_hookimpl
|
||||
from .hookspec import account_hookimpl, Global
|
||||
|
||||
|
||||
class ImexFailed(RuntimeError):
|
||||
"""Exception for signalling that import/export operations failed."""
|
||||
""" Exception for signalling that import/export operations failed."""
|
||||
|
||||
|
||||
class ImexTracker:
|
||||
@@ -23,15 +24,14 @@ class ImexTracker:
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, int) and ev >= target_progress:
|
||||
assert ev <= progress_upper_limit, (
|
||||
assert ev <= progress_upper_limit, \
|
||||
str(ev) + " exceeded upper progress limit " + str(progress_upper_limit)
|
||||
)
|
||||
return ev
|
||||
if ev == 0:
|
||||
return None
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
"""Return list of written files, raise ValueError if ExportFailed."""
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
@@ -44,7 +44,7 @@ class ImexTracker:
|
||||
|
||||
|
||||
class ConfigureFailed(RuntimeError):
|
||||
"""Exception for signalling that configuration failed."""
|
||||
""" Exception for signalling that configuration failed."""
|
||||
|
||||
|
||||
class ConfigureTracker:
|
||||
@@ -77,11 +77,11 @@ class ConfigureTracker:
|
||||
self.account.remove_account_plugin(self)
|
||||
|
||||
def wait_smtp_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
""" wait until smtp is configured. """
|
||||
self._smtp_finished.wait()
|
||||
|
||||
def wait_imap_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
""" wait until smtp is configured. """
|
||||
self._imap_finished.wait()
|
||||
|
||||
def wait_progress(self, data1=None):
|
||||
@@ -91,7 +91,7 @@ class ConfigureTracker:
|
||||
break
|
||||
|
||||
def wait_finish(self, timeout=None):
|
||||
"""wait until configure is completed.
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
workspacedir = sys.argv[1]
|
||||
@@ -11,13 +13,5 @@ if __name__ == "__main__":
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"auditwheel",
|
||||
"repair",
|
||||
p,
|
||||
"-w",
|
||||
workspacedir,
|
||||
"--plat",
|
||||
"manylinux2014_" + arch,
|
||||
]
|
||||
)
|
||||
["auditwheel", "repair", p, "-w", workspacedir,
|
||||
"--plat", "manylinux2014_" + arch])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from queue import Empty, Queue
|
||||
|
||||
import time
|
||||
import threading
|
||||
import pytest
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
|
||||
import deltachat
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_db_busy_error(acfactory, tmpdir):
|
||||
for acc in accounts:
|
||||
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
|
||||
with open(acc.bigfile, "wb") as f:
|
||||
f.write(b"01234567890" * 1000_000)
|
||||
f.write(b"01234567890"*1000_000)
|
||||
log("created %s bigfiles" % len(accounts))
|
||||
|
||||
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
|
||||
@@ -63,7 +63,9 @@ def test_db_busy_error(acfactory, tmpdir):
|
||||
elif report_type == ReportType.message_echo:
|
||||
continue
|
||||
else:
|
||||
raise ValueError("{} unknown report type {}, args={}".format(addr, report_type, report_args))
|
||||
raise ValueError("{} unknown report type {}, args={}".format(
|
||||
addr, report_type, report_args
|
||||
))
|
||||
alive_count -= 1
|
||||
replier.log("shutting down")
|
||||
replier.account.shutdown()
|
||||
@@ -86,7 +88,10 @@ class AutoReplier:
|
||||
self.current_sent = 0
|
||||
self.addr = self.account.get_self_contact().addr
|
||||
|
||||
self._thread = threading.Thread(name="Stats{}".format(self.account), target=self.thread_stats)
|
||||
self._thread = threading.Thread(
|
||||
name="Stats{}".format(self.account),
|
||||
target=self.thread_stats
|
||||
)
|
||||
self._thread.setDaemon(True)
|
||||
self._thread.start()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -278,7 +277,7 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
|
||||
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
|
||||
group_chat = [c for c in chats if c.get_name() == "group name"][0]
|
||||
assert group_chat.is_group()
|
||||
(private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
|
||||
private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
|
||||
assert not private_chat.is_group()
|
||||
|
||||
group_messages = group_chat.get_messages()
|
||||
@@ -379,7 +378,7 @@ def test_ephemeral_timer(acfactory, lp):
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {"Enabled": {"duration": 60}}
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import queue
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
import pytest
|
||||
from deltachat import const
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
@@ -79,7 +78,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
|
||||
assert len(export_files) == 2
|
||||
for x in export_files:
|
||||
assert x.startswith(dir.strpath)
|
||||
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
key_id, = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
ac1._evtracker.consume_events()
|
||||
|
||||
lp.sec("exported keys (private and public)")
|
||||
@@ -87,7 +86,8 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
|
||||
lp.indent(dir.strpath + os.sep + name)
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(dir.strpath)
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False)
|
||||
key_id2, = ac2._evtracker.get_info_regex_groups(
|
||||
r".*stored.*KeyId\((.*)\).*", check_error=False)
|
||||
assert key_id2 == key_id
|
||||
|
||||
|
||||
@@ -238,70 +238,6 @@ def test_html_message(acfactory, lp):
|
||||
assert html_text in msg2.html
|
||||
|
||||
|
||||
def test_videochat_invitation_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_videochat_invitation()
|
||||
|
||||
lp.sec("ac1: prepare and send videochat invitation to ac2")
|
||||
msg1 = Message.new_empty(ac1, "videochat")
|
||||
msg1.set_text(text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == text
|
||||
assert msg2.is_videochat_invitation()
|
||||
|
||||
|
||||
def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_webxdc()
|
||||
assert not msg1.send_status_update({"payload": "not an webxdc"}, "invalid")
|
||||
assert not msg1.get_status_updates()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_webxdc()
|
||||
assert not msg1.get_status_updates()
|
||||
|
||||
lp.sec("ac1: prepare and send webxdc instance to ac2")
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
assert msg1.send_status_update({"payload": "test1"}, "some test data")
|
||||
assert msg1.send_status_update({"payload": "test2"}, "more test data")
|
||||
assert len(msg1.get_status_updates()) == 2
|
||||
update1 = msg1.get_status_updates()[0]
|
||||
assert update1["payload"] == "test1"
|
||||
assert len(msg1.get_status_updates(update1["serial"])) == 1
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert msg2.is_webxdc()
|
||||
assert msg2.filename
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
@@ -753,7 +689,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "first message"
|
||||
assert not msg.is_encrypted()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config('addr'))
|
||||
assert msg.chat.get_encryption_info() == res
|
||||
lp.sec("ac2 learns that ac3 prefers encryption")
|
||||
ac2.create_chat(ac3)
|
||||
@@ -765,7 +701,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
lp.sec("ac3 does not know that ac1 prefers encryption")
|
||||
ac1.create_chat(ac3)
|
||||
chat = ac3.create_chat(ac1)
|
||||
res = "No encryption:\n{}".format(ac1.get_config("addr"))
|
||||
res = "No encryption:\n{}\n".format(ac1.get_config('addr'))
|
||||
assert chat.get_encryption_info() == res
|
||||
msg = chat.send_text("not encrypted")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
@@ -776,7 +712,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
encryption_info = group_chat.get_encryption_info()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config("addr"))
|
||||
assert encryption_info == res
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
@@ -830,7 +766,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
|
||||
def test_no_draft_if_cant_send(acfactory):
|
||||
"""Tests that no quote can be set if the user can't send to this chat"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
device_chat = ac1.get_device_chat()
|
||||
msg = Message.new_empty(ac1, "text")
|
||||
device_chat.set_draft(msg)
|
||||
@@ -859,9 +795,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.stop_io()
|
||||
|
||||
ac1.direct_imap.append(
|
||||
"Drafts",
|
||||
"""
|
||||
ac1.direct_imap.append("Drafts", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
@@ -869,13 +803,8 @@ def test_dont_show_emails(acfactory, lp):
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Sent",
|
||||
"""
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
@@ -883,13 +812,8 @@ def test_dont_show_emails(acfactory, lp):
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Sent
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Spam", """
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
@@ -897,13 +821,8 @@ def test_dont_show_emails(acfactory, lp):
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Junk", """
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
@@ -911,10 +830,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
@@ -1238,7 +1154,7 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
@@ -1471,7 +1387,8 @@ def test_add_remove_member_remote_events(acfactory, lp):
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
assert chat.is_promoted()
|
||||
assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
|
||||
assert sorted(x.addr for x in chat.get_contacts()) == \
|
||||
sorted(x.addr for x in ev.chat.get_contacts())
|
||||
|
||||
lp.sec("ac1: add address2")
|
||||
# note that if the above create_chat() would not
|
||||
@@ -1613,10 +1530,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
+ "all messages are fetched"
|
||||
)
|
||||
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " +
|
||||
"all messages are fetched")
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
@@ -1679,12 +1594,10 @@ def test_fetch_deleted_msg(acfactory, lp):
|
||||
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2429.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1.direct_imap.append(
|
||||
"INBOX",
|
||||
"""
|
||||
ac1.direct_imap.append("INBOX", """
|
||||
From: alice <alice@example.org>
|
||||
Subject: subj
|
||||
To: bob@example.com
|
||||
@@ -1693,8 +1606,7 @@ def test_fetch_deleted_msg(acfactory, lp):
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Deleted message
|
||||
""",
|
||||
)
|
||||
""")
|
||||
ac1.direct_imap.delete("1:*", expunge=False)
|
||||
ac1.start_io()
|
||||
|
||||
@@ -1976,26 +1888,11 @@ def test_group_quote(acfactory, lp):
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"folder,move,expected_destination,",
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails are recognized in a random folder but not moved
|
||||
(
|
||||
"xyz",
|
||||
True,
|
||||
"DeltaChat",
|
||||
), # ...emails are found in a random folder and moved to DeltaChat
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
"INBOX",
|
||||
), # ...emails are moved from the spam folder to the Inbox
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
|
||||
@@ -2,13 +2,13 @@ from __future__ import print_function
|
||||
|
||||
import os.path
|
||||
import shutil
|
||||
from filecmp import cmp
|
||||
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
|
||||
|
||||
def wait_msg_delivered(account, msg_list):
|
||||
"""wait for one or more MSG_DELIVERED events to match msg_list contents."""
|
||||
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
|
||||
msg_list = list(msg_list)
|
||||
while msg_list:
|
||||
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
@@ -16,7 +16,7 @@ def wait_msg_delivered(account, msg_list):
|
||||
|
||||
|
||||
def wait_msgs_changed(account, msgs_list):
|
||||
"""wait for one or more MSGS_CHANGED events to match msgs_list contents."""
|
||||
""" wait for one or more MSGS_CHANGED events to match msgs_list contents. """
|
||||
account.log("waiting for msgs_list={}".format(msgs_list))
|
||||
msgs_list = list(msgs_list)
|
||||
while msgs_list:
|
||||
@@ -38,7 +38,7 @@ class TestOnlineInCreation:
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt").ensure(file=1)
|
||||
src = tmpdir.join('file.txt').ensure(file=1)
|
||||
with pytest.raises(Exception):
|
||||
chat.prepare_message_file(src.strpath)
|
||||
|
||||
@@ -48,11 +48,11 @@ class TestOnlineInCreation:
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt")
|
||||
src = tmpdir.join('file.txt')
|
||||
src.write("hello there\n")
|
||||
chat.send_file(src.strpath)
|
||||
|
||||
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
|
||||
blob_src = os.path.join(ac1.get_blobdir(), 'file.txt')
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
@@ -63,7 +63,7 @@ class TestOnlineInCreation:
|
||||
|
||||
lp.sec("create a message with a file in creation")
|
||||
orig = data.get_path("d.png")
|
||||
path = os.path.join(ac1.get_blobdir(), "d.png")
|
||||
path = os.path.join(ac1.get_blobdir(), 'd.png')
|
||||
with open(path, "x") as fp:
|
||||
fp.write("preparing")
|
||||
prepared_original = chat.prepare_message_file(path)
|
||||
@@ -94,7 +94,10 @@ class TestOnlineInCreation:
|
||||
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
||||
|
||||
lp.sec("wait for both messages to be delivered to SMTP")
|
||||
wait_msg_delivered(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
|
||||
wait_msg_delivered(ac1, [
|
||||
(chat2.id, forwarded_id),
|
||||
(chat.id, prepared_original.id)
|
||||
])
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
received_original = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -1,51 +1,32 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat import Account, const
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
from deltachat.tracker import ImexFailed
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msgtext,res",
|
||||
[
|
||||
(
|
||||
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
(
|
||||
"Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me"),
|
||||
),
|
||||
(
|
||||
"Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org"),
|
||||
),
|
||||
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
(
|
||||
"Member tmp1@x.org added by tmp2@x.org.",
|
||||
("added", "tmp1@x.org", "tmp2@x.org"),
|
||||
),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
|
||||
@@ -155,11 +136,9 @@ class TestOfflineContact:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact3 = None
|
||||
str(contact1)
|
||||
repr(contact1)
|
||||
assert contact1 == contact2
|
||||
assert contact1 != contact3
|
||||
assert contact1.id
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
@@ -253,12 +232,10 @@ class TestOfflineChat:
|
||||
def test_chat_idempotent(self, chat1, ac1):
|
||||
contact1 = chat1.get_contacts()[0]
|
||||
chat2 = contact1.create_chat()
|
||||
chat3 = None
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
assert chat1 != chat3
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
if ichat.id == chat1.id:
|
||||
@@ -410,8 +387,6 @@ class TestOfflineChat:
|
||||
|
||||
def test_message_eq_contains(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
msg2 = None
|
||||
assert msg != msg2
|
||||
assert msg in chat1.get_messages()
|
||||
assert not (msg not in chat1.get_messages())
|
||||
str(msg)
|
||||
@@ -449,14 +424,11 @@ class TestOfflineChat:
|
||||
assert os.path.exists(msg.filename)
|
||||
assert msg.filemime == "image/png"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"typein,typeout",
|
||||
[
|
||||
@pytest.mark.parametrize("typein,typeout", [
|
||||
(None, "application/octet-stream"),
|
||||
("text/plain", "text/plain"),
|
||||
("image/png", "image/png"),
|
||||
],
|
||||
)
|
||||
])
|
||||
def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
|
||||
lp.sec("sending file")
|
||||
fn = data.get_path("r.txt")
|
||||
@@ -503,7 +475,7 @@ class TestOfflineChat:
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_config("addr", "123@example.org")
|
||||
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
|
||||
def test_import_export_one_contact(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
@@ -532,162 +504,6 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
|
||||
passphrase1 = "passphrase1"
|
||||
passphrase2 = "passphrase2"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
ac2.open(passphrase2)
|
||||
ac2.import_all(path)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
# check that passphrase is not lost after import:
|
||||
ac2 = Account(ac2.db_path, logging=ac2._logging, closed=True)
|
||||
ac2.open(passphrase2)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmpdir):
|
||||
passphrase = "test_passphrase"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ImexFailed):
|
||||
ac2.import_all(path, wrong_passphrase)
|
||||
ac2.import_all(path, passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
|
||||
"""
|
||||
Test that account passphrase isn't lost if backup failed to be imported.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/3379
|
||||
"""
|
||||
acct_passphrase = "passphrase1"
|
||||
bak_passphrase = "passphrase2"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, bak_passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
ac2.open(acct_passphrase)
|
||||
with pytest.raises(ImexFailed):
|
||||
ac2.import_all(path, wrong_passphrase)
|
||||
ac2.import_all(path, bak_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
# check that passphrase is not lost after import
|
||||
ac2 = Account(ac2.db_path, logging=ac2._logging, closed=True)
|
||||
ac2.open(acct_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg1 = chat1.prepare_message(msg)
|
||||
@@ -813,6 +629,6 @@ class TestOfflineChat:
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref,
|
||||
lib.dc_array_unref
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
|
||||
import os
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from deltachat import capi, const, cutil, register_global_plugin
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat import capi, cutil, const
|
||||
from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.testplugin import (
|
||||
ACSetup,
|
||||
create_dict_from_files_in_path,
|
||||
write_dict_to_dir,
|
||||
)
|
||||
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
class TestACSetup:
|
||||
|
||||
def test_cache_writing(self, tmp_path):
|
||||
base = tmp_path.joinpath("hello")
|
||||
base.mkdir()
|
||||
d1 = base.joinpath("dir1")
|
||||
d1.mkdir()
|
||||
d1.joinpath("file1").write_bytes(b"content1")
|
||||
d1.joinpath("file1").write_bytes(b'content1')
|
||||
d2 = d1.joinpath("dir2")
|
||||
d2.mkdir()
|
||||
d2.joinpath("file2").write_bytes(b"123")
|
||||
@@ -104,7 +103,6 @@ def test_dc_close_events(tmpdir, acfactory):
|
||||
def dc_account_after_shutdown(self, account):
|
||||
assert account._dc_context is None
|
||||
shutdowns.put(account)
|
||||
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
@@ -180,8 +178,8 @@ def test_get_info_open(tmpdir):
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert "deltachat_core_version" in info
|
||||
assert "database_dir" in info
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' in info
|
||||
|
||||
|
||||
def test_logged_hook_failure(acfactory):
|
||||
@@ -189,7 +187,7 @@ def test_logged_hook_failure(acfactory):
|
||||
cap = []
|
||||
ac1.log = cap.append
|
||||
with ac1._event_thread.swallow_and_log_exception("some"):
|
||||
0 / 0
|
||||
0/0
|
||||
assert cap
|
||||
assert "some" in str(cap)
|
||||
assert "ZeroDivisionError" in str(cap)
|
||||
@@ -204,7 +202,7 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
class FailPlugin:
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(ffi_event):
|
||||
0 / 0
|
||||
0/0
|
||||
|
||||
cap = Queue()
|
||||
ac1.log = cap.put
|
||||
|
||||
@@ -36,14 +36,10 @@ skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
flake8
|
||||
isort
|
||||
black
|
||||
# pygments required by rst-lint
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
isort --check src/deltachat examples/ tests/
|
||||
black --check src/deltachat examples/ tests/
|
||||
flake8 src/deltachat
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
@@ -89,4 +85,3 @@ markers =
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
ignore = E203, E266, E501, W503
|
||||
@@ -1 +1 @@
|
||||
1.61.0
|
||||
1.60.0
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# 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.60.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# 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.60.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -28,8 +28,6 @@ rm -f python3.8
|
||||
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||
rm -f python3.9
|
||||
ln -s /opt/python/cp39-cp39/bin/python3.9
|
||||
rm -f python3.10
|
||||
ln -s /opt/python/cp310-cp310/bin/python3.10
|
||||
popd
|
||||
|
||||
pushd python
|
||||
@@ -43,7 +41,7 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,auditwheels
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
|
||||
@@ -141,10 +141,9 @@ impl Accounts {
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self
|
||||
.accounts
|
||||
.remove(&id)
|
||||
.with_context(|| format!("no account with id {}", id))?;
|
||||
let ctx = self.accounts.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
@@ -446,7 +445,7 @@ impl Config {
|
||||
let id = {
|
||||
let id = self.inner.next_id;
|
||||
let uuid = Uuid::new_v4();
|
||||
let target_dir = dir.join(uuid.to_string());
|
||||
let target_dir = dir.join(uuid.to_simple_ref().to_string());
|
||||
|
||||
self.inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
|
||||
@@ -289,7 +289,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Returns the filename of the blob.
|
||||
pub fn as_file_name(&self) -> &str {
|
||||
self.name.rsplit('/').next().unwrap_or_default()
|
||||
self.name.rsplit('/').next().unwrap()
|
||||
}
|
||||
|
||||
/// The path relative in the blob directory.
|
||||
@@ -1083,7 +1083,8 @@ mod tests {
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
149
src/chat.rs
149
src/chat.rs
@@ -41,7 +41,7 @@ use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
@@ -438,7 +438,6 @@ impl ChatId {
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -787,7 +786,7 @@ impl ChatId {
|
||||
);
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
&query,
|
||||
query,
|
||||
paramsv![
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
@@ -896,7 +895,7 @@ impl ChatId {
|
||||
ret += &ret_mutual;
|
||||
}
|
||||
|
||||
Ok(ret.trim().to_string())
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
@@ -1042,9 +1041,7 @@ impl Chat {
|
||||
if chat.id.is_archived_link() {
|
||||
chat.name = stock_str::archived_chats(context).await;
|
||||
} else {
|
||||
if chat.typ == Chattype::Single && chat.name.is_empty() {
|
||||
// chat.name is set to contact.display_name on changes,
|
||||
// however, if things went wrong somehow, we do this here explicitly.
|
||||
if chat.typ == Chattype::Single {
|
||||
let mut chat_name = "Err [Name not found]".to_owned();
|
||||
match get_chat_contacts(context, chat.id).await {
|
||||
Ok(contacts) => {
|
||||
@@ -1391,7 +1388,11 @@ impl Chat {
|
||||
} else {
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
};
|
||||
html.map(|html| new_html_mimepart(html).build().as_string())
|
||||
if let Some(html) = html {
|
||||
Some(new_html_mimepart(html).await.build().as_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1951,7 +1952,7 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
}
|
||||
}
|
||||
msg.param.remove(Param::PrepForwards);
|
||||
msg.update_param(context).await?;
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
return send_msg_inner(context, chat_id, msg).await;
|
||||
}
|
||||
@@ -2068,7 +2069,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
@@ -2078,7 +2079,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
"End-to-end-encryption unavailable unexpectedly.",
|
||||
Some("End-to-end-encryption unavailable unexpectedly."),
|
||||
)
|
||||
.await;
|
||||
bail!(
|
||||
@@ -2120,7 +2121,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
|
||||
if rendered_msg.is_encrypted && !needs_encryption {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.update_param(context).await?;
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
|
||||
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
|
||||
@@ -2128,7 +2129,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
let recipients = recipients.join(" ");
|
||||
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.update_subject(context).await?;
|
||||
msg.update_subject(context).await;
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -3060,7 +3061,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
@@ -3117,7 +3118,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
msg.update_param(context).await;
|
||||
msg.param = save_param;
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
@@ -3165,7 +3166,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await?;
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
@@ -3377,7 +3378,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
/// Adds an informational message to chat.
|
||||
///
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -3387,7 +3387,6 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
// Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
|
||||
timestamp_sent_rcvd: Option<i64>,
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
@@ -3403,7 +3402,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
|
||||
paramsv![
|
||||
chat_id,
|
||||
from_id.unwrap_or(ContactId::INFO),
|
||||
ContactId::INFO,
|
||||
ContactId::INFO,
|
||||
timestamp_sort,
|
||||
timestamp_sent_rcvd.unwrap_or(0),
|
||||
@@ -3439,29 +3438,10 @@ pub(crate) async fn add_info_msg(
|
||||
timestamp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn update_msg_text_and_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
text: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
|
||||
paramsv![text, timestamp, msg_id],
|
||||
)
|
||||
.await?;
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -3696,11 +3676,12 @@ mod tests {
|
||||
|
||||
// create group and sync it to the second device
|
||||
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
|
||||
a1.send_text(a1_chat_id, "ho!").await;
|
||||
send_text_msg(&a1, a1_chat_id, "ho!".to_string()).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_chat_id = a2_msg.chat_id;
|
||||
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
|
||||
|
||||
@@ -3719,7 +3700,8 @@ mod tests {
|
||||
add_contact_to_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3732,7 +3714,8 @@ mod tests {
|
||||
set_chat_name(&a1, a1_chat_id, "bar").await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3745,7 +3728,8 @@ mod tests {
|
||||
remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3808,9 +3792,10 @@ mod tests {
|
||||
bob.recv_msg(&add3).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob_chat_id = bob.recv_msg(&add2).await.chat_id;
|
||||
bob.recv_msg(&add2).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
|
||||
|
||||
bob.recv_msg(&remove2).await;
|
||||
@@ -3879,11 +3864,12 @@ mod tests {
|
||||
|
||||
// Alice sends first message to group.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Bob leaves the group.
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
@@ -4538,7 +4524,6 @@ mod tests {
|
||||
10000,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4952,7 +4937,8 @@ mod tests {
|
||||
let mime = sent_msg.payload();
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_filename(), Some(filename.to_string()));
|
||||
@@ -5014,16 +5000,18 @@ mod tests {
|
||||
|
||||
// send sticker to bob
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
|
||||
// forward said sticker to alice
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&forwarded_msg).await;
|
||||
|
||||
// retrieve forwarded sticker which should not have forwarded-flag
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let msg = alice.recv_msg(&forwarded_msg).await;
|
||||
// forwarded sticker should not have forwarded-flag
|
||||
assert!(!msg.is_forwarded());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5037,12 +5025,15 @@ mod tests {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
let msg = alice.recv_msg(&forwarded_msg).await;
|
||||
alice.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.get_text().unwrap() == "Hi Bob");
|
||||
assert!(msg.is_forwarded());
|
||||
Ok(())
|
||||
@@ -5057,19 +5048,23 @@ mod tests {
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text(Some("Reply".to_owned()));
|
||||
reply.set_quote(&bob, Some(&received_msg)).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
let received_reply = alice.recv_msg(&sent_reply).await;
|
||||
alice.recv_msg(&sent_reply).await;
|
||||
let received_reply = alice.get_last_msg().await;
|
||||
|
||||
// Alice forwards a reply.
|
||||
forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?;
|
||||
let forwarded_msg = alice.pop_sent_msg().await;
|
||||
let alice_forwarded_msg = bob.recv_msg(&forwarded_msg).await;
|
||||
bob.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let alice_forwarded_msg = alice.get_last_msg().await;
|
||||
assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none());
|
||||
assert_eq!(
|
||||
alice_forwarded_msg.quoted_text(),
|
||||
@@ -5101,7 +5096,8 @@ mod tests {
|
||||
let sent_group_msg = alice
|
||||
.send_text(alice_group_chat_id, "Hi Bob and Claire")
|
||||
.await;
|
||||
let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id;
|
||||
bob.recv_msg(&sent_group_msg).await;
|
||||
let bob_group_chat_id = bob.get_last_msg().await.chat_id;
|
||||
|
||||
// Alice deletes a message on her device.
|
||||
// This is needed to make assignment of further messages received in this group
|
||||
@@ -5111,13 +5107,15 @@ mod tests {
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Alice sends another message to Bob, this has first message as a parent.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
@@ -5154,7 +5152,8 @@ mod tests {
|
||||
// Bob forwards that message to Claire -
|
||||
// Claire should not get information about Alice for the original Group
|
||||
let bob = TestContext::new_bob().await;
|
||||
let orig_msg = bob.recv_msg(&sent_msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let orig_msg = bob.get_last_msg().await;
|
||||
let claire_id = Contact::create(&bob, "claire", "claire@foo").await?;
|
||||
let single_id = ChatId::create_for_contact(&bob, claire_id).await?;
|
||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||
@@ -5201,16 +5200,15 @@ mod tests {
|
||||
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
let received = bob.recv_msg_opt(&sent3).await;
|
||||
// No message should actually be added since we already know this message:
|
||||
assert!(received.is_none());
|
||||
bob.recv_msg(&sent3).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
|
||||
@@ -5218,7 +5216,8 @@ mod tests {
|
||||
let claire = TestContext::new().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
let msg = claire.recv_msg(&sent3).await;
|
||||
claire.recv_msg(&sent3).await;
|
||||
let msg = claire.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0).await?.len(), 2);
|
||||
@@ -5241,7 +5240,8 @@ mod tests {
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -5262,7 +5262,8 @@ mod tests {
|
||||
|
||||
// Bob now can send an encrypted message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
msg.chat_id.accept(&bob).await?;
|
||||
@@ -5346,7 +5347,8 @@ mod tests {
|
||||
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// test broadcast list
|
||||
@@ -5366,7 +5368,8 @@ mod tests {
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), Some("ola!".to_string()));
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
@@ -5426,7 +5429,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net"
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_fiona).await?;
|
||||
@@ -5434,7 +5437,7 @@ mod tests {
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n\
|
||||
fiona@example.net"
|
||||
fiona@example.net\n"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
@@ -5447,7 +5450,7 @@ mod tests {
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption preferred:\n\
|
||||
bob@example.net"
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
@@ -5460,7 +5463,7 @@ mod tests {
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net"
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
|
||||
use crate::events::EventType;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
@@ -89,11 +89,6 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
FetchExistingMsgs,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
/// This flag is reset after successful configuration.
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
@@ -360,8 +355,6 @@ impl Context {
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
|
||||
// add old primary address (if exists) to secondary addresses
|
||||
let mut secondary_addrs = self.get_all_self_addrs().await?;
|
||||
// never store a primary address also as a secondary
|
||||
@@ -375,17 +368,6 @@ impl Context {
|
||||
self.set_config(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
|
||||
if let Some(old_addr) = old_addr {
|
||||
let old_addr = EmailAddress::new(&old_addr)?;
|
||||
let old_keypair = crate::key::load_keypair(self, &old_addr).await?;
|
||||
|
||||
if let Some(mut old_keypair) = old_keypair {
|
||||
old_keypair.addr = EmailAddress::new(primary_new)?;
|
||||
crate::key::store_self_keypair(self, &old_keypair, crate::key::KeyPairUse::Default)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -437,9 +419,7 @@ mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
@@ -519,14 +499,14 @@ mod tests {
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
|
||||
|
||||
// Test adding a new (primary) self address
|
||||
// The address is trimmed during configure by `LoginParam::from_database()`,
|
||||
// The address is trimmed during by `LoginParam::from_database()`,
|
||||
// so `set_primary_self_addr()` doesn't have to trim it.
|
||||
alice.set_primary_self_addr("Alice@alice.com").await?;
|
||||
assert!(alice.is_self_addr("aliCe@example.org").await?);
|
||||
alice.set_primary_self_addr(" Alice@alice.com ").await?;
|
||||
assert!(alice.is_self_addr(" aliCe@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["Alice@alice.com", "Alice@Example.Org"]
|
||||
vec![" Alice@alice.com ", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Check that the entry is not duplicated
|
||||
@@ -557,68 +537,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Alice sends a message to Bob
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
assert_eq!(bob_msg.text.unwrap(), "Hi");
|
||||
|
||||
// Alice changes her self address and reconfigures
|
||||
// (ensure_secret_key_exists() is called during configure)
|
||||
alice
|
||||
.set_primary_self_addr("alice@someotherdomain.xyz")
|
||||
.await?;
|
||||
crate::e2ee::ensure_secret_key_exists(&alice).await?;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_primary_self_addr().await?,
|
||||
"alice@someotherdomain.xyz"
|
||||
);
|
||||
|
||||
// Bob sends a message to Alice, encrypting to her previous key
|
||||
let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
|
||||
|
||||
// Alice set up message forwarding so that she still receives
|
||||
// the message with her new address
|
||||
let alice_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(alice_msg.text, Some("hi back".to_string()));
|
||||
assert_eq!(alice_msg.get_showpadlock(), true);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
// Even if Bob sends a message to Alice without In-Reply-To,
|
||||
// it's still assigned to the 1:1 chat with Bob and not to
|
||||
// a group (without secondary addresses, an ad-hoc group
|
||||
// would be created)
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.net
|
||||
To: alice@example.org
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <456@example.com>
|
||||
|
||||
Message w/out In-Reply-To
|
||||
",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
alice_msg.text,
|
||||
Some("Message w/out In-Reply-To".to_string())
|
||||
);
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -19,8 +20,8 @@ use crate::job;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::dc_get_oauth2_addr;
|
||||
use crate::param::Params;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::{chat, e2ee, provider};
|
||||
@@ -468,9 +469,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
|
||||
.await?;
|
||||
ctx.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
job::add(
|
||||
ctx,
|
||||
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await?;
|
||||
|
||||
121
src/contact.rs
121
src/contact.rs
@@ -562,7 +562,7 @@ impl Contact {
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if update_name || update_authname {
|
||||
if update_name {
|
||||
// Update the contact name also if it is used as a group name.
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id: Option<i32> = context.sql.query_get_value(
|
||||
@@ -699,7 +699,7 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr NOT IN ({})
|
||||
@@ -754,7 +754,7 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr NOT IN ({})
|
||||
AND id>?
|
||||
@@ -1443,8 +1443,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
@@ -1685,118 +1684,6 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_contact_name_changes() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// first message creates contact and one-to-one-chat without name set
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: f@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <1234-1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 29 May 2022 08:37:57 +0000\n\
|
||||
\n\
|
||||
hello one\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
chat_id.accept(&t).await?;
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "f@example.org");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("f@example.org"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = contacts.first().unwrap();
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "f@example.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "f@example.org");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// second message inits the name
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Flobbyfoo <f@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <1234-2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 29 May 2022 08:38:57 +0000\n\
|
||||
\n\
|
||||
hello two\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// third message changes the name
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Foo Flobby <f@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <1234-3@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 29 May 2022 08:39:57 +0000\n\
|
||||
\n\
|
||||
hello three\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Foo Flobby");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("Flobbyfoo"), None).await?;
|
||||
assert_eq!(chatlist.len(), 0);
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// change name manually
|
||||
let test_id = Contact::create(&t, "Falk", "f@example.org").await?;
|
||||
assert_eq!(*contact_id, test_id);
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "Falk");
|
||||
assert_eq!(contact.get_display_name(), "Falk");
|
||||
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
123
src/context.rs
123
src/context.rs
@@ -5,7 +5,7 @@ use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::time::{Instant, SystemTime};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
path::{Path, PathBuf},
|
||||
@@ -44,7 +44,7 @@ pub struct InnerContext {
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
running_state: RwLock<RunningState>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
pub(crate) generating_key_mutex: Mutex<()>,
|
||||
/// Mutex to enforce only a single running oauth2 is running.
|
||||
@@ -76,23 +76,11 @@ pub struct InnerContext {
|
||||
pub(crate) last_error: RwLock<String>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
#[derive(Debug)]
|
||||
enum RunningState {
|
||||
/// Ongoing process is allocated.
|
||||
Running { cancel_sender: Sender<()> },
|
||||
|
||||
/// Cancel signal has been sent, waiting for ongoing process to be freed.
|
||||
ShallStop,
|
||||
|
||||
/// There is no ongoing process, a new one can be allocated.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl Default for RunningState {
|
||||
fn default() -> Self {
|
||||
Self::Stopped
|
||||
}
|
||||
pub struct RunningState {
|
||||
ongoing_running: bool,
|
||||
shall_stop_ongoing: bool,
|
||||
cancel_sender: Option<Sender<()>>,
|
||||
}
|
||||
|
||||
/// Return some info about deltachat-core
|
||||
@@ -291,46 +279,45 @@ impl Context {
|
||||
|
||||
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
let mut s = self.running_state.write().await;
|
||||
ensure!(
|
||||
matches!(*s, RunningState::Stopped),
|
||||
"There is already another ongoing process running."
|
||||
);
|
||||
if s.ongoing_running || !s.shall_stop_ongoing {
|
||||
bail!("There is already another ongoing process running.");
|
||||
}
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
let (sender, receiver) = channel::bounded(1);
|
||||
*s = RunningState::Running {
|
||||
cancel_sender: sender,
|
||||
};
|
||||
s.cancel_sender = Some(sender);
|
||||
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub(crate) async fn free_ongoing(&self) {
|
||||
let mut s = self.running_state.write().await;
|
||||
*s = RunningState::Stopped;
|
||||
|
||||
s.ongoing_running = false;
|
||||
s.shall_stop_ongoing = true;
|
||||
s.cancel_sender.take();
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
pub async fn stop_ongoing(&self) {
|
||||
let mut s = self.running_state.write().await;
|
||||
match &*s {
|
||||
RunningState::Running { cancel_sender } => {
|
||||
if let Err(err) = cancel_sender.send(()).await {
|
||||
warn!(self, "could not cancel ongoing: {:?}", err);
|
||||
}
|
||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||
*s = RunningState::ShallStop;
|
||||
}
|
||||
RunningState::ShallStop | RunningState::Stopped => {
|
||||
info!(self, "No ongoing process to stop.",);
|
||||
if let Some(cancel) = s.cancel_sender.take() {
|
||||
if let Err(err) = cancel.send(()).await {
|
||||
warn!(self, "could not cancel ongoing: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if s.ongoing_running && !s.shall_stop_ongoing {
|
||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||
s.shall_stop_ongoing = true;
|
||||
} else {
|
||||
info!(self, "No ongoing process to stop.",);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
match &*self.running_state.read().await {
|
||||
RunningState::Running { .. } => false,
|
||||
RunningState::ShallStop | RunningState::Stopped => true,
|
||||
}
|
||||
self.running_state.read().await.shall_stop_ongoing
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -433,12 +420,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"fetched_existing_msgs",
|
||||
self.get_config_bool(Config::FetchedExistingMsgs)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
@@ -654,6 +635,16 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RunningState {
|
||||
fn default() -> Self {
|
||||
RunningState {
|
||||
ongoing_running: false,
|
||||
shall_stop_ongoing: true,
|
||||
cancel_sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
@@ -1053,44 +1044,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// No ongoing process allocated.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
let receiver = context.alloc_ongoing().await?;
|
||||
|
||||
// Cannot allocate another ongoing process while the first one is running.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
// Stop signal is not sent yet.
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
assert!(!context.shall_stop_ongoing().await);
|
||||
|
||||
// Send the stop signal.
|
||||
context.stop_ongoing().await;
|
||||
|
||||
// Receive stop signal.
|
||||
receiver.recv().await?;
|
||||
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Ongoing process is still running even though stop signal was received,
|
||||
// so another one cannot be allocated.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// No ongoing process allocated, should have been stopped already.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Another ongoing process can be allocated now.
|
||||
let _receiver = context.alloc_ongoing().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,16 @@ pub struct ReceivedMsg {
|
||||
pub chat_id: ChatId,
|
||||
pub state: MessageState,
|
||||
pub sort_timestamp: i64,
|
||||
// Feel free to add more fields here
|
||||
}
|
||||
|
||||
/// Information about added message parts.
|
||||
struct AddedParts {
|
||||
/// Common info about received messages.
|
||||
pub received_msg: ReceivedMsg,
|
||||
|
||||
/// IDs of inserted rows in messages table.
|
||||
pub msg_ids: Vec<MsgId>,
|
||||
pub created_db_entries: Vec<MsgId>,
|
||||
|
||||
/// Whether IMAP messages should be immediately deleted.
|
||||
pub needs_delete_job: bool,
|
||||
@@ -179,7 +186,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
|
||||
|
||||
// Add parts
|
||||
let received_msg = add_parts(
|
||||
let added_parts = add_parts(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
imf_raw,
|
||||
@@ -204,7 +211,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
// Update gossiped timestamp for the chat if someone else or our other device sent
|
||||
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
|
||||
// and waste traffic.
|
||||
let chat_id = received_msg.chat_id;
|
||||
let chat_id = added_parts.received_msg.chat_id;
|
||||
if !chat_id.is_special()
|
||||
&& mime_parser
|
||||
.recipients
|
||||
@@ -222,7 +229,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
|
||||
let insert_msg_id = if let Some(msg_id) = added_parts.created_db_entries.last() {
|
||||
*msg_id
|
||||
} else {
|
||||
MsgId::new_unset()
|
||||
@@ -246,7 +253,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
|
||||
if let Some(ref status_update) = mime_parser.webxdc_status_update {
|
||||
if let Err(err) = context
|
||||
.receive_status_update(from_id, insert_msg_id, status_update)
|
||||
.receive_status_update(insert_msg_id, status_update)
|
||||
.await
|
||||
{
|
||||
warn!(context, "receive_imf cannot update status: {}", err);
|
||||
@@ -303,8 +310,8 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
||||
|
||||
if !received_msg.msg_ids.is_empty() {
|
||||
if received_msg.needs_delete_job
|
||||
if !added_parts.created_db_entries.is_empty() {
|
||||
if added_parts.needs_delete_job
|
||||
|| (delete_server_after == Some(0) && is_partial_download.is_none())
|
||||
{
|
||||
context
|
||||
@@ -323,12 +330,12 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
if replace_partial_download {
|
||||
context.emit_msgs_changed(chat_id, MsgId::new(0));
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh;
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
let fresh = added_parts.received_msg.state == MessageState::InFresh;
|
||||
for msg_id in added_parts.created_db_entries {
|
||||
if incoming && fresh {
|
||||
context.emit_incoming_msg(chat_id, *msg_id);
|
||||
context.emit_incoming_msg(chat_id, msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(chat_id, *msg_id);
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -337,7 +344,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.handle_reports(context, from_id, sent_timestamp, &mime_parser.parts)
|
||||
.await;
|
||||
|
||||
Ok(Some(received_msg))
|
||||
Ok(Some(added_parts.received_msg))
|
||||
}
|
||||
|
||||
/// Converts "From" field to contact id.
|
||||
@@ -401,7 +408,7 @@ async fn add_parts(
|
||||
is_partial_download: Option<u32>,
|
||||
fetching_existing_messages: bool,
|
||||
prevent_rename: bool,
|
||||
) -> Result<ReceivedMsg> {
|
||||
) -> Result<AddedParts> {
|
||||
let mut chat_id = None;
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
@@ -1174,18 +1181,15 @@ INSERT INTO msgs
|
||||
}
|
||||
}
|
||||
|
||||
if !incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
|
||||
// Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
|
||||
// outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
|
||||
// delete it.
|
||||
needs_delete_job = true;
|
||||
}
|
||||
|
||||
Ok(ReceivedMsg {
|
||||
let received_msg = ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
sort_timestamp,
|
||||
msg_ids: created_db_entries,
|
||||
};
|
||||
|
||||
Ok(AddedParts {
|
||||
received_msg,
|
||||
created_db_entries,
|
||||
needs_delete_job,
|
||||
})
|
||||
}
|
||||
@@ -1437,9 +1441,7 @@ async fn create_or_lookup_group(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let grpname = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.context("Chat-Group-Name vanished")?;
|
||||
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).unwrap();
|
||||
let new_chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
@@ -2006,7 +2008,7 @@ async fn check_verified_properties(
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
sql::repeat_vars(to_ids.len())
|
||||
@@ -2650,7 +2652,7 @@ mod tests {
|
||||
"shenauithz@testrun.org",
|
||||
"Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it",
|
||||
include_bytes!("../test-data/message/tiscali_ndn.eml"),
|
||||
Some("Non-Delivery-Notification without further details."),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -3745,7 +3747,8 @@ YEAAAAAA!.
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), Some("hi!".to_string()));
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
@@ -3764,23 +3767,25 @@ YEAAAAAA!.
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), Some("hi!".to_string()));
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("Received:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
|
||||
// another one, from bob to alice, that gets encrypted
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), Some("ho!".to_string()));
|
||||
assert!(msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&alice, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("Received:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -4465,14 +4470,16 @@ Second thread."#;
|
||||
let alice_first_reply = alice
|
||||
.send_text(alice_first_msg.chat_id, "First reply")
|
||||
.await;
|
||||
let bob_first_reply = bob.recv_msg(&alice_first_reply).await;
|
||||
bob.recv_msg(&alice_first_reply).await;
|
||||
let bob_first_reply = bob.get_last_msg().await;
|
||||
assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id);
|
||||
|
||||
alice_second_msg.chat_id.accept(&alice).await?;
|
||||
let alice_second_reply = alice
|
||||
.send_text(alice_second_msg.chat_id, "Second reply")
|
||||
.await;
|
||||
let bob_second_reply = bob.recv_msg(&alice_second_reply).await;
|
||||
bob.recv_msg(&alice_second_reply).await;
|
||||
let bob_second_reply = bob.get_last_msg().await;
|
||||
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
|
||||
|
||||
// Alice adds Fiona to both ad hoc groups.
|
||||
@@ -4487,11 +4494,13 @@ Second thread."#;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_first_invite = alice.pop_sent_msg().await;
|
||||
let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await;
|
||||
fiona.recv_msg(&alice_first_invite).await;
|
||||
let fiona_first_invite = fiona.get_last_msg().await;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_second_invite = alice.pop_sent_msg().await;
|
||||
let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await;
|
||||
fiona.recv_msg(&alice_second_invite).await;
|
||||
let fiona_second_invite = fiona.get_last_msg().await;
|
||||
|
||||
// Fiona was added to two separate chats and should see two separate chats, even though they
|
||||
// don't have different group IDs to distinguish them.
|
||||
@@ -4773,7 +4782,8 @@ Reply from different address
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await;
|
||||
println!("{}", sent.payload());
|
||||
|
||||
let msg_bob = bob.recv_msg(&sent).await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let msg_bob = bob.get_last_msg().await;
|
||||
|
||||
async fn check_message(msg: &Message, t: &TestContext, content: &str) {
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
@@ -4813,7 +4823,9 @@ Reply from different address
|
||||
|
||||
alice1.recv_msg(&sent).await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let alice1_msg = bob2.recv_msg(&sent).await;
|
||||
bob2.recv_msg(&sent).await;
|
||||
|
||||
let alice1_msg = alice1.get_last_msg().await;
|
||||
assert_eq!(alice1_msg.text.unwrap(), "Hello!");
|
||||
let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?;
|
||||
assert!(alice1_chat.is_contact_request());
|
||||
|
||||
@@ -13,7 +13,7 @@ use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Error, Result};
|
||||
use anyhow::Error;
|
||||
use chrono::{Local, TimeZone};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
@@ -443,6 +443,14 @@ pub(crate) fn time() -> i64 {
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// An invalid email address was encountered
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid email address: {message} ({addr})")]
|
||||
pub struct InvalidEmailError {
|
||||
message: String,
|
||||
addr: String,
|
||||
}
|
||||
|
||||
/// Very simple email address wrapper.
|
||||
///
|
||||
/// Represents an email address, right now just the `name@domain` portion.
|
||||
@@ -466,7 +474,7 @@ pub struct EmailAddress {
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(input: &str) -> Result<Self> {
|
||||
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
|
||||
input.parse::<EmailAddress>()
|
||||
}
|
||||
}
|
||||
@@ -478,12 +486,18 @@ impl fmt::Display for EmailAddress {
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
type Err = InvalidEmailError;
|
||||
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
fn from_str(input: &str) -> Result<EmailAddress> {
|
||||
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
|
||||
let err = |msg: &str| {
|
||||
Err(InvalidEmailError {
|
||||
message: msg.to_string(),
|
||||
addr: input.to_string(),
|
||||
})
|
||||
};
|
||||
if input.is_empty() {
|
||||
bail!("empty string is not valid");
|
||||
return err("empty string is not valid");
|
||||
}
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
@@ -491,23 +505,23 @@ impl FromStr for EmailAddress {
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
return err("Email must not contain whitespaces, '>' or '<'");
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
return err("empty string is not valid for local part");
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
return err("missing domain after '@'");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
_ => err("missing '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fn dehtml_quick_xml(buf: &str) -> String {
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => dehtml_text_cb(&e.escape(), &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(ref e)) => dehtml_cdata_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
@@ -134,6 +134,23 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
}
|
||||
}
|
||||
|
||||
fn dehtml_cdata_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
|
||||
} else if !dehtml.line_prefix().is_empty() {
|
||||
let l = dehtml.append_prefix("\n");
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
|
||||
} else {
|
||||
dehtml.strbuilder += &last_added;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
|
||||
27
src/e2ee.rs
27
src/e2ee.rs
@@ -171,6 +171,7 @@ pub async fn try_decrypt(
|
||||
}
|
||||
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
@@ -184,11 +185,17 @@ pub async fn try_decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let (out_mail, signatures) =
|
||||
match decrypt_if_autocrypt_message(context, mail, public_keyring_for_validate).await? {
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
let (out_mail, signatures) = match decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
@@ -286,6 +293,7 @@ fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMai
|
||||
async fn decrypt_if_autocrypt_message(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail)
|
||||
@@ -299,9 +307,6 @@ async fn decrypt_if_autocrypt_message(
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
|
||||
.await
|
||||
.context("failed to get own keyring")?;
|
||||
|
||||
decrypt_part(
|
||||
encrypted_data_part,
|
||||
@@ -315,7 +320,7 @@ async fn decrypt_if_autocrypt_message(
|
||||
///
|
||||
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
fn validate_detached_signature(
|
||||
async fn validate_detached_signature(
|
||||
mail: &ParsedMail<'_>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
@@ -328,7 +333,7 @@ fn validate_detached_signature(
|
||||
let content = first_part.raw_bytes;
|
||||
let signature = second_part.get_body_raw()?;
|
||||
let ret_valid_signatures =
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate)?;
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate).await?;
|
||||
|
||||
Ok(Some((content.to_vec(), ret_valid_signatures)))
|
||||
} else {
|
||||
@@ -352,7 +357,7 @@ async fn decrypt_part(
|
||||
// If decrypted part is a multipart/signed, then there is a detached signature.
|
||||
let decrypted_part = mailparse::parse_mail(&plain)?;
|
||||
if let Some((content, valid_detached_signatures)) =
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate)?
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate).await?
|
||||
{
|
||||
return Ok(Some((content, valid_detached_signatures)));
|
||||
} else {
|
||||
|
||||
@@ -76,7 +76,7 @@ use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use std::cmp::max;
|
||||
|
||||
@@ -303,11 +303,15 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let msg_ids: Vec<&dyn crate::ToSql> = msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id as &dyn crate::ToSql)
|
||||
.collect();
|
||||
let now = time();
|
||||
let count = context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
@@ -316,7 +320,7 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::ToSql))
|
||||
.chain(params_iter(msg_ids)),
|
||||
.chain(msg_ids),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
31
src/error.rs
Normal file
31
src/error.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! # Error handling
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_eq {
|
||||
($left:expr, $right:expr) => ({
|
||||
match (&$left, &$right) {
|
||||
(left_val, right_val) => {
|
||||
if !(*left_val == *right_val) {
|
||||
bail!(r#"assertion failed: `(left == right)`
|
||||
left: `{:?}`,
|
||||
right: `{:?}`"#, left_val, right_val)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
($left:expr, $right:expr,) => ({
|
||||
ensure_eq!($left, $right)
|
||||
});
|
||||
($left:expr, $right:expr, $($arg:tt)+) => ({
|
||||
match (&($left), &($right)) {
|
||||
(left_val, right_val) => {
|
||||
if !(*left_val == *right_val) {
|
||||
bail!(r#"assertion failed: `(left == right)`
|
||||
left: `{:?}`,
|
||||
right: `{:?}`: {}"#, left_val, right_val,
|
||||
format_args!($($arg)+))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
//! # Events specification.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_std::channel::{self, Receiver, Sender, TrySendError};
|
||||
use async_std::path::PathBuf;
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
@@ -89,6 +92,8 @@ impl async_std::stream::Stream for EventEmitter {
|
||||
/// context emits them in relation to various operations happening, a lot of these are again
|
||||
/// documented in `deltachat.h`.
|
||||
///
|
||||
/// This struct [`Deref`]s to the [`EventType`].
|
||||
///
|
||||
/// [`Context`]: crate::context::Context
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Event {
|
||||
@@ -105,39 +110,68 @@ pub struct Event {
|
||||
pub typ: EventType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
impl Deref for Event {
|
||||
type Target = EventType;
|
||||
|
||||
fn deref(&self) -> &EventType {
|
||||
&self.typ
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType {
|
||||
/// Returns the corresponding Event ID.
|
||||
///
|
||||
/// These are the IDs used in the `DC_EVENT_*` constants in `deltachat.h`.
|
||||
pub fn as_id(&self) -> i32 {
|
||||
self.get_str("id")
|
||||
.expect("missing id")
|
||||
.parse()
|
||||
.expect("invalid id")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
|
||||
pub enum EventType {
|
||||
/// 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.
|
||||
#[strum(props(id = "100"))]
|
||||
Info(String),
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
#[strum(props(id = "101"))]
|
||||
SmtpConnected(String),
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
#[strum(props(id = "102"))]
|
||||
ImapConnected(String),
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
#[strum(props(id = "103"))]
|
||||
SmtpMessageSent(String),
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
#[strum(props(id = "104"))]
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
#[strum(props(id = "105"))]
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
#[strum(props(id = "150"))]
|
||||
NewBlobFile(String),
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
#[strum(props(id = "151"))]
|
||||
DeletedBlobFile(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.
|
||||
#[strum(props(id = "300"))]
|
||||
Warning(String),
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
@@ -150,6 +184,7 @@ pub enum EventType {
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a messasge box then.
|
||||
#[strum(props(id = "400"))]
|
||||
Error(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
@@ -157,6 +192,7 @@ pub enum EventType {
|
||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||
/// dc_send_text_msg() or another sending function.
|
||||
#[strum(props(id = "410"))]
|
||||
ErrorSelfNotInGroup(String),
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
@@ -167,44 +203,35 @@ pub enum EventType {
|
||||
///
|
||||
/// `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msg_id` is set if only a single message is affected by the changes, otherwise 0.
|
||||
MsgsChanged {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
#[strum(props(id = "2000"))]
|
||||
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
/// 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.
|
||||
IncomingMsg {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
#[strum(props(id = "2005"))]
|
||||
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[strum(props(id = "2008"))]
|
||||
MsgsNoticed(ChatId),
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||
MsgDelivered {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
#[strum(props(id = "2010"))]
|
||||
MsgDelivered { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
/// 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 dc_msg_get_state().
|
||||
MsgFailed {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
#[strum(props(id = "2012"))]
|
||||
MsgFailed { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||
MsgRead {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
#[strum(props(id = "2015"))]
|
||||
MsgRead { chat_id: ChatId, msg_id: MsgId },
|
||||
|
||||
/// 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.
|
||||
@@ -213,9 +240,11 @@ pub enum EventType {
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[strum(props(id = "2020"))]
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[strum(props(id = "2021"))]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: ChatId,
|
||||
timer: EphemeralTimer,
|
||||
@@ -224,6 +253,7 @@ pub enum EventType {
|
||||
/// 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.
|
||||
#[strum(props(id = "2030"))]
|
||||
ContactsChanged(Option<ContactId>),
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
@@ -231,9 +261,11 @@ pub enum EventType {
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
#[strum(props(id = "2035"))]
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
#[strum(props(id = "2041"))]
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
@@ -248,6 +280,7 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
#[strum(props(id = "2051"))]
|
||||
ImexProgress(usize),
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
@@ -257,6 +290,7 @@ pub enum EventType {
|
||||
/// services.
|
||||
///
|
||||
/// @param data2 0
|
||||
#[strum(props(id = "2052"))]
|
||||
ImexFileWritten(PathBuf),
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
@@ -271,6 +305,7 @@ pub enum EventType {
|
||||
/// 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.
|
||||
#[strum(props(id = "2060"))]
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
@@ -284,6 +319,7 @@ pub enum EventType {
|
||||
/// @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)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
@@ -293,10 +329,13 @@ pub enum EventType {
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
||||
/// dc_get_connectivity_html() for details.
|
||||
#[strum(props(id = "2100"))]
|
||||
ConnectivityChanged,
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
|
||||
#[strum(props(id = "2120"))]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: MsgId,
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
|
||||
31
src/html.rs
31
src/html.rs
@@ -11,7 +11,7 @@ use futures::future::FutureExt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -64,7 +64,7 @@ enum MimeMultipartType {
|
||||
|
||||
/// Function takes a content type from a ParsedMail structure
|
||||
/// and checks and returns the rough mime-type.
|
||||
fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
|
||||
async fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
|
||||
let mimetype = ctype.mimetype.to_lowercase();
|
||||
if mimetype.starts_with("multipart") && ctype.params.get("boundary").is_some() {
|
||||
MimeMultipartType::Multiple
|
||||
@@ -122,7 +122,7 @@ impl HtmlMsgParser {
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
match get_mime_multipart_type(&mail.ctype).await {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in mail.subparts.iter() {
|
||||
self.collect_texts_recursive(context, cur_data).await?
|
||||
@@ -134,7 +134,7 @@ impl HtmlMsgParser {
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
self.collect_texts_recursive(context, &mail).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
@@ -178,7 +178,7 @@ impl HtmlMsgParser {
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
match get_mime_multipart_type(&mail.ctype).await {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in mail.subparts.iter() {
|
||||
self.cid_to_data_recursive(context, cur_data).await?;
|
||||
@@ -190,7 +190,7 @@ impl HtmlMsgParser {
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
self.cid_to_data_recursive(context, &mail).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
@@ -198,7 +198,7 @@ impl HtmlMsgParser {
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
|
||||
if let Ok(cid) = parse_message_id(&cid) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail).await {
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
@@ -233,7 +233,7 @@ impl HtmlMsgParser {
|
||||
}
|
||||
|
||||
/// Convert a mime part to a data: url as defined in [RFC 2397](https://tools.ietf.org/html/rfc2397).
|
||||
fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
|
||||
async fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
|
||||
let data = mail.get_body_raw()?;
|
||||
let data = base64::encode(&data);
|
||||
Ok(format!("data:{};base64,{}", mail.ctype.mimetype, data))
|
||||
@@ -267,7 +267,7 @@ impl MsgId {
|
||||
///
|
||||
/// Used on forwarding messages to avoid leaking the original mime structure
|
||||
/// and also to avoid sending too much, maybe large data.
|
||||
pub fn new_html_mimepart(html: String) -> PartBuilder {
|
||||
pub async fn new_html_mimepart(html: String) -> PartBuilder {
|
||||
PartBuilder::new()
|
||||
.content_type(&"text/html; charset=utf-8".parse::<mime::Mime>().unwrap())
|
||||
.body(html)
|
||||
@@ -467,8 +467,8 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
@@ -503,8 +503,9 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// receive the message on another device
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
|
||||
let msg = alice.recv_msg(&msg).await;
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
alice.recv_msg(&msg).await;
|
||||
let chat = alice.get_self_chat().await;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.get_showpadlock());
|
||||
@@ -538,8 +539,8 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// let bob receive the message
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_text(), Some("plain text".to_string()));
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
|
||||
107
src/imap.rs
107
src/imap.rs
@@ -24,7 +24,7 @@ use crate::constants::{
|
||||
Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
|
||||
DC_LP_AUTH_OAUTH2,
|
||||
};
|
||||
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::{
|
||||
dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, ReceivedMsg,
|
||||
@@ -125,14 +125,6 @@ enum FolderMeaning {
|
||||
Sent,
|
||||
Drafts,
|
||||
Other,
|
||||
|
||||
/// Virtual folders.
|
||||
///
|
||||
/// On Gmail there are virtual folders marked as \\All, \\Important and \\Flagged.
|
||||
/// Delta Chat ignores these folders because the same messages can be fetched
|
||||
/// from the real folder and the result of moving and deleting messages via
|
||||
/// virtual folder is unclear.
|
||||
Virtual,
|
||||
}
|
||||
|
||||
impl FolderMeaning {
|
||||
@@ -143,7 +135,6 @@ impl FolderMeaning {
|
||||
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
|
||||
FolderMeaning::Drafts => None,
|
||||
FolderMeaning::Other => None,
|
||||
FolderMeaning::Virtual => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -905,42 +896,6 @@ impl Imap {
|
||||
Ok(read_cnt > 0)
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
|
||||
/// and show them in the chat list.
|
||||
pub(crate) async fn fetch_existing_msgs(&mut self, context: &Context) -> Result<()> {
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
return Ok(()); // Bots don't want those messages
|
||||
}
|
||||
self.prepare(context).await.context("could not connect")?;
|
||||
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder).await;
|
||||
|
||||
if context.get_config_bool(Config::FetchExistingMsgs).await? {
|
||||
for config in &[
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Config::ConfiguredInboxFolder,
|
||||
Config::ConfiguredSentboxFolder,
|
||||
] {
|
||||
if let Some(folder) = context.get_config(*config).await? {
|
||||
self.fetch_new_messages(context, &folder, false, true)
|
||||
.await
|
||||
.context("could not fetch messages")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
context
|
||||
.set_config_bool(Config::FetchedExistingMsgs, true)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes batch of messages identified by their UID from the currently
|
||||
/// selected folder.
|
||||
async fn delete_message_batch(
|
||||
@@ -955,7 +910,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -992,7 +947,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -1034,7 +989,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"UPDATE imap SET target='' WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -1075,14 +1030,9 @@ impl Imap {
|
||||
.await?;
|
||||
|
||||
self.prepare(context).await?;
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
// Select folder inside the loop to avoid selecting it if there are no pending
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
@@ -1151,7 +1101,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"DELETE FROM imap_markseen WHERE id IN ({})",
|
||||
sql::repeat_vars(rowid_set.len())
|
||||
),
|
||||
@@ -1933,7 +1883,6 @@ fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
|
||||
"\\Sent" => return FolderMeaning::Sent,
|
||||
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
|
||||
"\\Drafts" => return FolderMeaning::Drafts,
|
||||
"\\All" | "\\Important" | "\\Flagged" => return FolderMeaning::Virtual,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -2323,50 +2272,6 @@ impl std::fmt::Display for UidRange {
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
|
||||
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
|
||||
m
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not select {}: {:#}", mailbox, e);
|
||||
return;
|
||||
}
|
||||
match imap.get_all_recipients(context).await {
|
||||
Ok(contacts) => {
|
||||
let mut any_modified = false;
|
||||
for contact in contacts {
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
&contact.addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
any_modified = true;
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipient: {}", e),
|
||||
}
|
||||
}
|
||||
if any_modified {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipients: {}", e),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -35,14 +35,16 @@ impl Imap {
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
for folder in folders {
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
if folder_meaning == FolderMeaning::Virtual {
|
||||
// Gmail has virtual folders that should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
// already been moved and left it in the inbox.
|
||||
// Gmail labels are not folders and should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
// already been moved and left it in the inbox.
|
||||
let folder_name = folder.name();
|
||||
if folder_name.starts_with("[Gmail]") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
|
||||
if let Some(config) = folder_meaning.to_config() {
|
||||
|
||||
69
src/imex.rs
69
src/imex.rs
@@ -19,8 +19,8 @@ use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{
|
||||
dc_create_folder, dc_delete_file, dc_get_filesuffix_lc, dc_open_file_std, dc_read_file,
|
||||
dc_write_file, time, EmailAddress,
|
||||
dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
|
||||
dc_open_file_std, dc_read_file, dc_write_file, time, EmailAddress,
|
||||
};
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
@@ -95,6 +95,7 @@ pub async fn imex(
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
cleanup_aborted_imex(context, what).await;
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
error!(context, "{:#}", err);
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
@@ -104,6 +105,7 @@ pub async fn imex(
|
||||
}
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
cleanup_aborted_imex(context, what).await;
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await;
|
||||
@@ -113,6 +115,13 @@ pub async fn imex(
|
||||
res
|
||||
}
|
||||
|
||||
async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
|
||||
if what == ImexMode::ImportBackup {
|
||||
dc_delete_file(context, context.get_dbfile()).await;
|
||||
dc_delete_files_in_dir(context, context.get_blobdir()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||
@@ -922,17 +931,6 @@ mod tests {
|
||||
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path().as_ref()).await?;
|
||||
|
||||
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
|
||||
assert!(imex(
|
||||
&context2,
|
||||
ImexMode::ImportBackup,
|
||||
backup.as_ref(),
|
||||
Some("foobar".to_string())
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
@@ -989,49 +987,4 @@ mod tests {
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
assert!(headers.get(HEADER_SETUPCODE).is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let alice_clone = alice.clone();
|
||||
let key_transfer_task = async_std::task::spawn(async move {
|
||||
let ctx = alice_clone;
|
||||
initiate_key_transfer(&ctx).await
|
||||
});
|
||||
|
||||
// Wait for the message to be added to the queue.
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let setup_code = key_transfer_task.await?;
|
||||
|
||||
// Alice sets up a second device.
|
||||
let alice2 = TestContext::new().await;
|
||||
alice2.set_name("alice2");
|
||||
alice2.configure_addr("alice@example.org").await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
|
||||
// Send a message that cannot be decrypted because the keys are
|
||||
// not synchronized yet.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_ne!(
|
||||
alice.get_last_msg().await.get_text(),
|
||||
Some("Test".to_string())
|
||||
);
|
||||
|
||||
// Transfer the key.
|
||||
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
alice.get_last_msg().await.get_text(),
|
||||
Some("Test".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
95
src/job.rs
95
src/job.rs
@@ -8,8 +8,11 @@ use anyhow::{Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::Imap;
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
@@ -55,6 +58,8 @@ macro_rules! job_try {
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Action {
|
||||
FetchExistingMsgs = 110,
|
||||
|
||||
// this is user initiated so it should have a fairly high priority
|
||||
UpdateRecentQuota = 140,
|
||||
|
||||
@@ -151,6 +156,45 @@ impl Job {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
|
||||
/// and show them in the chat list.
|
||||
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if job_try!(context.get_config_bool(Config::Bot).await) {
|
||||
return Status::Finished(Ok(())); // Bots don't want those messages
|
||||
}
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredSentboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
|
||||
|
||||
if job_try!(context.get_config_bool(Config::FetchExistingMsgs).await) {
|
||||
for config in &[
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Config::ConfiguredInboxFolder,
|
||||
Config::ConfiguredSentboxFolder,
|
||||
] {
|
||||
if let Some(folder) = job_try!(context.get_config(*config).await) {
|
||||
if let Err(e) = imap.fetch_new_messages(context, &folder, false, true).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not fetch messages, retrying: {:#}", e);
|
||||
return Status::RetryLater;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
/// Synchronizes UIDs for all folders.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
@@ -206,6 +250,51 @@ pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
|
||||
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
|
||||
m
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not select {}: {:#}", mailbox, e);
|
||||
return;
|
||||
}
|
||||
match imap.get_all_recipients(context).await {
|
||||
Ok(contacts) => {
|
||||
let mut any_modified = false;
|
||||
for contact in contacts {
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
&contact.addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
any_modified = true;
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipient: {}", e),
|
||||
}
|
||||
}
|
||||
if any_modified {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipients: {}", e),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) enum Connection<'a> {
|
||||
Inbox(&'a mut Imap),
|
||||
}
|
||||
@@ -282,6 +371,7 @@ async fn perform_job_action(
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
|
||||
Ok(status) => status,
|
||||
Err(err) => Status::Finished(Err(err)),
|
||||
@@ -332,7 +422,10 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
|
||||
if delay_seconds == 0 {
|
||||
match action {
|
||||
Action::ResyncFolders | Action::UpdateRecentQuota | Action::DownloadMsg => {
|
||||
Action::ResyncFolders
|
||||
| Action::FetchExistingMsgs
|
||||
| Action::UpdateRecentQuota
|
||||
| Action::DownloadMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
}
|
||||
|
||||
217
src/key.rs
217
src/key.rs
@@ -3,10 +3,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use futures::Future;
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
@@ -26,6 +25,7 @@ pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||
/// [SignedSecretKey] types and makes working with them a little
|
||||
/// easier in the deltachat world.
|
||||
#[async_trait]
|
||||
pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
type KeyType: Serialize + Deserializable + KeyTrait + Clone;
|
||||
|
||||
@@ -54,9 +54,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
}
|
||||
|
||||
/// Load the users' default key from the database.
|
||||
fn load_self<'a>(
|
||||
context: &'a Context,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>>;
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType>;
|
||||
|
||||
/// Serialise the key as bytes.
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
@@ -84,42 +82,39 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
|
||||
/// The fingerprint for the key.
|
||||
fn fingerprint(&self) -> Fingerprint {
|
||||
Fingerprint::new(KeyTrait::fingerprint(self))
|
||||
Fingerprint::new(KeyTrait::fingerprint(self)).expect("Invalid fingerprint from rpgp")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
fn load_self<'a>(
|
||||
context: &'a Context,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>> {
|
||||
Box::pin(async move {
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=?
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(bytes) => Self::from_slice(&bytes),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=?
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(bytes) => Self::from_slice(&bytes),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
@@ -139,37 +134,34 @@ impl DcKey for SignedPublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
|
||||
fn load_self<'a>(
|
||||
context: &'a Context,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>> {
|
||||
Box::pin(async move {
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(bytes) => Self::from_slice(&bytes),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(bytes) => Self::from_slice(&bytes),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
@@ -212,8 +204,29 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match load_keypair(context, &addr).await? {
|
||||
Some(key_pair) => Ok(key_pair),
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||
Ok((pub_bytes, sec_bytes))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
None => {
|
||||
let start = std::time::SystemTime::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
|
||||
@@ -233,39 +246,6 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn load_keypair(
|
||||
context: &Context,
|
||||
addr: &EmailAddress,
|
||||
) -> Result<Option<KeyPair>> {
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||
Ok((pub_bytes, sec_bytes))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(if let Some((pub_bytes, sec_bytes)) = res {
|
||||
Some(KeyPair {
|
||||
addr: addr.clone(),
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Use of a [KeyPair] for encryption or decryption.
|
||||
///
|
||||
/// This is used by [store_self_keypair] to know what kind of key is
|
||||
@@ -295,20 +275,23 @@ pub async fn store_self_keypair(
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> Result<()> {
|
||||
let mut conn = context.sql.get_conn().await?;
|
||||
let transaction = conn.transaction()?;
|
||||
|
||||
// Everything should really be one transaction, more refactoring
|
||||
// is needed for that.
|
||||
let public_key = DcKey::to_bytes(&keypair.public);
|
||||
let secret_key = DcKey::to_bytes(&keypair.secret);
|
||||
transaction
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
)
|
||||
.await
|
||||
.context("failed to remove old use of key")?;
|
||||
if default == KeyPairUse::Default {
|
||||
transaction
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.context("failed to clear default")?;
|
||||
}
|
||||
let is_default = match default {
|
||||
@@ -319,16 +302,16 @@ pub async fn store_self_keypair(
|
||||
let addr = keypair.addr.to_string();
|
||||
let t = time();
|
||||
|
||||
transaction
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
paramsv![addr, is_default, public_key, secret_key, t],
|
||||
)
|
||||
.await
|
||||
.context("failed to insert keypair")?;
|
||||
|
||||
transaction.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -337,9 +320,11 @@ pub async fn store_self_keypair(
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
pub fn new(v: Vec<u8>) -> Result<Fingerprint> {
|
||||
match v.len() {
|
||||
20 => Ok(Fingerprint(v)),
|
||||
_ => Err(format_err!("Wrong fingerprint length")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a hex string from the fingerprint.
|
||||
@@ -379,15 +364,14 @@ impl fmt::Display for Fingerprint {
|
||||
impl std::str::FromStr for Fingerprint {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let hex_repr: String = input
|
||||
.to_uppercase()
|
||||
.chars()
|
||||
.filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c))
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
|
||||
let fp = Fingerprint::new(v);
|
||||
let v: Vec<u8> = hex::decode(hex_repr)?;
|
||||
let fp = Fingerprint::new(v)?;
|
||||
Ok(fp)
|
||||
}
|
||||
}
|
||||
@@ -605,7 +589,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_from_str() {
|
||||
let res = Fingerprint::new(vec![
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
]);
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let fp: Fingerprint = "0102030405060708090A0B0c0d0e0F1011121314".parse().unwrap();
|
||||
assert_eq!(fp, res);
|
||||
@@ -622,7 +607,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_hex() {
|
||||
let fp = Fingerprint::new(vec![
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
]);
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(fp.hex(), "0102040810204080FF0A0B0C0D0E0F1011121314");
|
||||
}
|
||||
|
||||
@@ -630,7 +616,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_to_string() {
|
||||
let fp = Fingerprint::new(vec![
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
|
||||
]);
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fp.to_string(),
|
||||
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
unused,
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless,
|
||||
clippy::unused_async
|
||||
clippy::cast_lossless
|
||||
)]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
@@ -25,6 +23,7 @@ extern crate num_derive;
|
||||
extern crate smallvec;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
extern crate strum;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
@@ -34,6 +33,8 @@ impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
#[macro_use]
|
||||
|
||||
@@ -153,12 +153,12 @@ impl Kml {
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
if tag == "document" {
|
||||
if let Some(addr) = event
|
||||
.attributes()
|
||||
.filter_map(|a| a.ok())
|
||||
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "addr")
|
||||
{
|
||||
self.addr = addr.unescape_and_decode_value(reader).ok();
|
||||
if let Some(addr) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "addr")
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
self.addr = addr.unwrap().unescape_and_decode_value(reader).ok();
|
||||
}
|
||||
} else if tag == "placemark" {
|
||||
self.tag = KmlTag::PLACEMARK;
|
||||
|
||||
@@ -210,7 +210,7 @@ impl LoginParam {
|
||||
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()
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
@@ -403,7 +404,7 @@ impl Message {
|
||||
}
|
||||
|
||||
if !self.id.is_unset() {
|
||||
self.update_param(context).await?;
|
||||
self.update_param(context).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,7 +762,7 @@ impl Message {
|
||||
width: i32,
|
||||
height: i32,
|
||||
duration: i32,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
if width > 0 && height > 0 {
|
||||
self.param.set_int(Param::Width, width);
|
||||
self.param.set_int(Param::Height, height);
|
||||
@@ -769,8 +770,7 @@ impl Message {
|
||||
if duration > 0 {
|
||||
self.param.set_int(Param::Duration, duration);
|
||||
}
|
||||
self.update_param(context).await?;
|
||||
Ok(())
|
||||
self.update_param(context).await;
|
||||
}
|
||||
|
||||
/// Sets message quote.
|
||||
@@ -850,26 +850,26 @@ impl Message {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
|
||||
pub async fn update_param(&self, context: &Context) -> Result<()> {
|
||||
pub async fn update_param(&self, context: &Context) {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?;",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
}
|
||||
|
||||
pub(crate) async fn update_subject(&self, context: &Context) -> Result<()> {
|
||||
pub(crate) async fn update_subject(&self, context: &Context) {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET subject=? WHERE id=?;",
|
||||
paramsv![self.subject, self.id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
}
|
||||
|
||||
/// Gets the error status of the message.
|
||||
@@ -1283,7 +1283,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
@@ -1424,8 +1424,9 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
|
||||
pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef<str>>) {
|
||||
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
|
||||
let error = error.map(|e| e.as_ref().to_string()).unwrap_or_default();
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg_id, error);
|
||||
@@ -1540,7 +1541,7 @@ pub async fn handle_mdn(
|
||||
pub(crate) async fn handle_ndn(
|
||||
context: &Context,
|
||||
failed: &FailureReport,
|
||||
error: &str,
|
||||
error: Option<impl AsRef<str>>,
|
||||
) -> Result<()> {
|
||||
if failed.rfc724_mid.is_empty() {
|
||||
return Ok(());
|
||||
@@ -1574,7 +1575,7 @@ pub(crate) async fn handle_ndn(
|
||||
let mut first = true;
|
||||
for msg in msgs.into_iter() {
|
||||
let (msg_id, chat_id, chat_type) = msg?;
|
||||
set_msg_failed(context, msg_id, error).await;
|
||||
set_msg_failed(context, msg_id, error.as_ref()).await;
|
||||
if first {
|
||||
// Add only one info msg for all failed messages
|
||||
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
|
||||
@@ -2089,8 +2090,8 @@ mod tests {
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&bob, contact_id).await.unwrap();
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.id).await;
|
||||
assert_eq!(msg.text, Some("bla blubb".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
@@ -2115,11 +2116,13 @@ mod tests {
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
|
||||
.await;
|
||||
let msg1 = bob.get_last_msg().await;
|
||||
let bob_chat_id = msg1.chat_id;
|
||||
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg2 = bob.recv_msg(&sent2).await;
|
||||
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = bob.get_last_msg().await;
|
||||
assert_eq!(msg1.chat_id, msg2.chat_id);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -2140,12 +2143,14 @@ mod tests {
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
let msg1 = alice
|
||||
alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice
|
||||
let msg1 = alice.get_last_msg().await;
|
||||
alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice.get_last_msg().await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
@@ -2217,11 +2222,12 @@ mod tests {
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
|
||||
set_msg_failed(&alice, alice_msg.id, "badly failed").await;
|
||||
set_msg_failed(&alice, alice_msg.id, Some("badly failed")).await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
let bob_msg = bob.recv_msg(&payload).await;
|
||||
bob.recv_msg(&payload).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_chat.id, bob_msg.chat_id);
|
||||
assert_state(&bob, bob_msg.id, MessageState::InFresh).await;
|
||||
|
||||
|
||||
@@ -1128,7 +1128,7 @@ impl<'a> MimeFactory<'a> {
|
||||
main_part = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(main_part.build())
|
||||
.child(new_html_mimepart(html).build());
|
||||
.child(new_html_mimepart(html).await.build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::future::Future;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
@@ -140,11 +140,7 @@ pub enum SystemMessage {
|
||||
|
||||
// Sync message that contains a json payload
|
||||
// sent to the other webxdc instances
|
||||
// These messages are not shown in the chat.
|
||||
WebxdcStatusUpdate = 30,
|
||||
|
||||
// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||
WebxdcInfoMessage = 32,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -269,15 +265,22 @@ impl MimeMessage {
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
|
||||
(Ok(decrypted_mail), signatures, true)
|
||||
(decrypted_mail, signatures, true)
|
||||
} else {
|
||||
// Message was not encrypted
|
||||
(Ok(mail), signatures, false)
|
||||
(mail, signatures, false)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// continue with the current, still encrypted, mime tree.
|
||||
// unencrypted parts will be replaced by an error message
|
||||
// that is added as "the message" to the chat then.
|
||||
//
|
||||
// if we just return here, the header is missing
|
||||
// and the caller cannot display the message
|
||||
// and try to assign the message to a chat
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(Err(err), Default::default(), true)
|
||||
(mail, Default::default(), true)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -288,7 +291,7 @@ impl MimeMessage {
|
||||
list_post,
|
||||
from,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: mail.is_err(),
|
||||
decrypting_failed: false,
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
@@ -315,24 +318,9 @@ impl MimeMessage {
|
||||
.create_stub_from_partial_download(context, org_bytes)
|
||||
.await?;
|
||||
}
|
||||
None => match mail {
|
||||
Ok(mail) => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
let msg_body = stock_str::cant_decrypt_msg_body(context).await;
|
||||
let txt = format!("[{}]", msg_body);
|
||||
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.clone()),
|
||||
msg: txt,
|
||||
error: Some(format!("Decrypting failed: {}", err)),
|
||||
..Default::default()
|
||||
};
|
||||
parser.parts.push(part);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
};
|
||||
|
||||
parser.maybe_remove_bad_parts();
|
||||
@@ -727,7 +715,7 @@ impl MimeMessage {
|
||||
if raw.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
|
||||
self.parse_mime_recursive(context, &mail, is_related).await
|
||||
}
|
||||
@@ -791,6 +779,25 @@ impl MimeMessage {
|
||||
self.is_mime_modified = true;
|
||||
}
|
||||
}
|
||||
(mime::MULTIPART, "encrypted") => {
|
||||
// we currently do not try to decrypt non-autocrypt messages
|
||||
// at all. If we see an encrypted part, we set
|
||||
// decrypting_failed.
|
||||
let msg_body = stock_str::cant_decrypt_msg_body(context).await;
|
||||
let txt = format!("[{}]", msg_body);
|
||||
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.clone()),
|
||||
msg: txt,
|
||||
error: Some("Decryption failed".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
self.parts.push(part);
|
||||
|
||||
any_part_added = true;
|
||||
self.decrypting_failed = true;
|
||||
}
|
||||
(mime::MULTIPART, "signed") => {
|
||||
/* RFC 1847: "The multipart/signed content type
|
||||
contains exactly two body parts. The first body
|
||||
@@ -1054,6 +1061,7 @@ impl MimeMessage {
|
||||
.unwrap_or_default();
|
||||
self.sync_items = context
|
||||
.parse_sync_items(serialized)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(context, "failed to parse sync data: {}", err);
|
||||
})
|
||||
@@ -1398,11 +1406,11 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map_or_else(
|
||||
|| "Non-Delivery-Notification without further details.".to_string(),
|
||||
|p| p.msg.clone(),
|
||||
);
|
||||
if let Err(e) = message::handle_ndn(context, failure_report, &error).await {
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
|
||||
warn!(context, "Could not handle ndn: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,10 +146,8 @@ pub async fn dc_get_oauth2_access_token(
|
||||
value = &redirect_uri;
|
||||
} else if value == "$CODE" {
|
||||
value = code;
|
||||
} else if value == "$REFRESH_TOKEN" {
|
||||
if let Some(refresh_token) = refresh_token.as_ref() {
|
||||
value = refresh_token;
|
||||
}
|
||||
} else if value == "$REFRESH_TOKEN" && refresh_token.is_some() {
|
||||
value = refresh_token.as_ref().unwrap();
|
||||
}
|
||||
|
||||
post_param.insert(key, value);
|
||||
@@ -164,18 +162,16 @@ pub async fn dc_get_oauth2_access_token(
|
||||
|
||||
let client = surf::Client::new();
|
||||
let parsed: Result<Response, _> = client.recv_json(req).await;
|
||||
let response = match parsed {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
if parsed.is_err() {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// update refresh_token if given, typically on the first round, but we update it later as well.
|
||||
let response = parsed.unwrap();
|
||||
if let Some(ref token) = response.refresh_token {
|
||||
context
|
||||
.sql
|
||||
@@ -290,13 +286,12 @@ impl Oauth2 {
|
||||
// }
|
||||
let response: Result<HashMap<String, serde_json::Value>, surf::Error> =
|
||||
surf::get(userinfo_url).recv_json().await;
|
||||
let parsed = match response {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
warn!(context, "Error getting userinfo: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if response.is_err() {
|
||||
warn!(context, "Error getting userinfo: {:?}", response);
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = response.unwrap();
|
||||
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// but serde_json::Value.to_string() does not!
|
||||
if let Some(addr) = parsed.get("email") {
|
||||
|
||||
@@ -267,11 +267,6 @@ impl Peerstate {
|
||||
context: &Context,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
if context.is_self_addr(&self.addr).await? {
|
||||
// Do not try to search all the chats with self.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.fingerprint_changed {
|
||||
if let Some(contact_id) = context
|
||||
.sql
|
||||
@@ -302,7 +297,6 @@ impl Peerstate {
|
||||
timestamp_sort,
|
||||
Some(timestamp),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ChatModified(*chat_id));
|
||||
|
||||
56
src/pgp.rs
56
src/pgp.rs
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
@@ -98,7 +98,9 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
let mut bytes = Vec::with_capacity(buf.len());
|
||||
|
||||
dearmor.read_to_end(&mut bytes)?;
|
||||
let typ = dearmor.typ.context("failed to parse type")?;
|
||||
ensure!(dearmor.typ.is_some(), "Failed to parse type");
|
||||
|
||||
let typ = dearmor.typ.unwrap();
|
||||
|
||||
// normalize headers
|
||||
let headers = dearmor
|
||||
@@ -110,6 +112,27 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
Ok((typ, headers, bytes))
|
||||
}
|
||||
|
||||
/// Error with generating a PGP keypair.
|
||||
///
|
||||
/// Most of these are likely coding errors rather than user errors
|
||||
/// since all variability is hardcoded.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("PgpKeygenError: {message}")]
|
||||
pub struct PgpKeygenError {
|
||||
message: String,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
}
|
||||
|
||||
impl PgpKeygenError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PGP keypair.
|
||||
///
|
||||
/// This has it's own struct to be able to keep the public and secret
|
||||
@@ -122,7 +145,10 @@ pub struct KeyPair {
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
|
||||
pub(crate) fn create_keypair(
|
||||
addr: EmailAddress,
|
||||
keygen_type: KeyGenType,
|
||||
) -> std::result::Result<KeyPair, PgpKeygenError> {
|
||||
let (secret_key_type, public_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
|
||||
@@ -157,32 +183,28 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
.build()
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("failed to build subkey parameters")?,
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("invalid key params")?;
|
||||
let key = key_params.generate().context("invalid params")?;
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!(err)))?;
|
||||
let key = key_params
|
||||
.generate()
|
||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||
let private_key = key
|
||||
.sign(|| "".into())
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("failed to sign secret key")?;
|
||||
.map_err(|err| PgpKeygenError::new("failed to sign secret key", err))?;
|
||||
|
||||
let public_key = private_key.public_key();
|
||||
let public_key = public_key
|
||||
.sign(&private_key, || "".into())
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("failed to sign public key")?;
|
||||
.map_err(|err| PgpKeygenError::new("failed to sign public key", err))?;
|
||||
|
||||
private_key
|
||||
.verify()
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("invalid private key generated")?;
|
||||
.map_err(|err| PgpKeygenError::new("invalid private key generated", err))?;
|
||||
public_key
|
||||
.verify()
|
||||
.map_err(|e| format_err!("{}", e))
|
||||
.context("invalid public key generated")?;
|
||||
.map_err(|err| PgpKeygenError::new("invalid public key generated", err))?;
|
||||
|
||||
Ok(KeyPair {
|
||||
addr,
|
||||
@@ -311,7 +333,7 @@ pub async fn pk_decrypt(
|
||||
}
|
||||
|
||||
/// Validates detached signature.
|
||||
pub fn pk_validate(
|
||||
pub async fn pk_validate(
|
||||
content: &[u8],
|
||||
signature: &[u8],
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
|
||||
@@ -514,7 +514,7 @@ static P_GMX_NET: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, hermes.radio
|
||||
// hermes.radio.md: hermes.radio
|
||||
static P_HERMES_RADIO: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "hermes.radio",
|
||||
status: Status::Ok,
|
||||
@@ -531,6 +531,10 @@ static P_HERMES_RADIO: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::E2eeEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::ShowEmails,
|
||||
value: "2",
|
||||
@@ -1615,38 +1619,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("gmx.info", &*P_GMX_NET),
|
||||
("gmx.biz", &*P_GMX_NET),
|
||||
("gmx.com", &*P_GMX_NET),
|
||||
("ac.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac1.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac2.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac3.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac4.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac5.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac6.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac7.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac8.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac9.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac10.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac11.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac12.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac13.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac14.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ac15.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka1.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka2.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka3.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka4.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka5.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka6.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka7.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka8.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka9.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka10.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka11.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka12.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka13.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka14.hermes.radio", &*P_HERMES_RADIO),
|
||||
("ka15.hermes.radio", &*P_HERMES_RADIO),
|
||||
("hermes.radio", &*P_HERMES_RADIO),
|
||||
("hey.com", &*P_HEY_COM),
|
||||
("i.ua", &*P_I_UA),
|
||||
@@ -1879,4 +1851,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 6, 4));
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 5, 3));
|
||||
|
||||
@@ -130,19 +130,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
}
|
||||
};
|
||||
|
||||
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
|
||||
Ok(fetched_existing_msgs) => {
|
||||
if !fetched_existing_msgs {
|
||||
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
|
||||
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
|
||||
}
|
||||
}
|
||||
|
||||
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +137,27 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("Failed to send handshake message: {0}")]
|
||||
SendMessage(#[from] SendMsgError),
|
||||
|
||||
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
|
||||
// is supposed to create a contact for us.
|
||||
#[error("Unknown contact (this is a bug): {0}")]
|
||||
UnknownContact(#[source] anyhow::Error),
|
||||
|
||||
#[error("Other")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
///
|
||||
/// This is the start of the process for the joiner. See the module and ffi documentation
|
||||
/// for more details.
|
||||
///
|
||||
/// The function returns immediately and the handshake will run in background.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
securejoin(context, qr).await.map_err(|err| {
|
||||
warn!(context, "Fatal joiner error: {:#}", err);
|
||||
// The user just scanned this QR code so has context on what failed.
|
||||
@@ -152,7 +166,7 @@ pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
@@ -166,6 +180,14 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
bob::start_protocol(context, invite).await
|
||||
}
|
||||
|
||||
/// Error when failing to send a protocol handshake message.
|
||||
///
|
||||
/// Wrapping the [anyhow::Error] means we can "impl From" more easily on errors from this
|
||||
/// function.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Failed sending handshake message")]
|
||||
pub struct SendMsgError(#[from] anyhow::Error);
|
||||
|
||||
/// Send handshake message from Alice's device;
|
||||
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
|
||||
async fn send_alice_handshake_msg(
|
||||
@@ -173,7 +195,7 @@ async fn send_alice_handshake_msg(
|
||||
contact_id: ContactId,
|
||||
step: &str,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
) -> Result<()> {
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(format!("Secure-Join: {}", step)),
|
||||
@@ -223,11 +245,8 @@ async fn fingerprint_equals_sender(
|
||||
};
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate
|
||||
.public_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|&fp| fp == fingerprint)
|
||||
.is_some()
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -331,8 +350,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed sending auth-required handshake message")?;
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
"vg-auth-required" | "vc-auth-required" => {
|
||||
@@ -460,8 +478,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"vc-contact-confirm",
|
||||
Some(fingerprint),
|
||||
)
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
.await?;
|
||||
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This are some helper functions around [`BobState`] which augment the state changes with
|
||||
//! the required user interactions.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
@@ -16,7 +16,7 @@ use crate::{chat, stock_str};
|
||||
|
||||
use super::bobstate::{BobHandshakeStage, BobState};
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::HandshakeMessage;
|
||||
use super::{HandshakeMessage, JoinError};
|
||||
|
||||
/// Starts the securejoin protocol with the QR `invite`.
|
||||
///
|
||||
@@ -30,7 +30,10 @@ use super::HandshakeMessage;
|
||||
///
|
||||
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
|
||||
/// chat with Alice, for a SecureJoin QR this is the group chat.
|
||||
pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
|
||||
pub(super) async fn start_protocol(
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<ChatId, JoinError> {
|
||||
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
|
||||
// hidden, if a user starts sending messages in it it will be unhidden in
|
||||
// dc_receive_imf.
|
||||
@@ -40,7 +43,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
};
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
|
||||
// Now start the protocol and initialise the state
|
||||
let (state, stage, aborted_states) =
|
||||
|
||||
@@ -22,7 +22,9 @@ use crate::param::Param;
|
||||
use crate::sql::Sql;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified};
|
||||
use super::{
|
||||
encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified, JoinError, SendMsgError,
|
||||
};
|
||||
|
||||
/// The stage of the [`BobState`] securejoin handshake protocol state machine.
|
||||
///
|
||||
@@ -92,7 +94,7 @@ impl BobState {
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>)> {
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>), JoinError> {
|
||||
let (stage, next) =
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await?
|
||||
{
|
||||
@@ -402,7 +404,11 @@ impl BobState {
|
||||
/// Sends the requested handshake message to Alice.
|
||||
///
|
||||
/// This takes care of adding the required headers for the step.
|
||||
async fn send_handshake_message(&self, context: &Context, step: BobHandshakeMsg) -> Result<()> {
|
||||
async fn send_handshake_message(
|
||||
&self,
|
||||
context: &Context,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<(), SendMsgError> {
|
||||
send_handshake_message(context, &self.invite, self.chat_id, step).await
|
||||
}
|
||||
}
|
||||
@@ -416,7 +422,7 @@ async fn send_handshake_message(
|
||||
invite: &QrInvite,
|
||||
chat_id: ChatId,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<()> {
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(step.body_text(invite)),
|
||||
|
||||
11
src/smtp.rs
11
src/smtp.rs
@@ -360,7 +360,7 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
if let SendResult::Failure(err) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
}
|
||||
status
|
||||
}
|
||||
@@ -410,7 +410,12 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
)
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
message::set_msg_failed(context, msg_id, "Number of retries exceeded the limit.").await;
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
Some("Number of retries exceeded the limit."),
|
||||
)
|
||||
.await;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
@@ -588,7 +593,7 @@ async fn send_mdn_msg_id(
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(&q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.execute(q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
77
src/sql.rs
77
src/sql.rs
@@ -146,21 +146,6 @@ impl Sql {
|
||||
.with_context(|| format!("path {:?} is not valid unicode", path))?;
|
||||
let conn = self.get_conn().await?;
|
||||
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
if let Err(err) = conn
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.context("backup passphrase is not correct")
|
||||
{
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Reset the database without reopening it. We don't want to reopen the database because we
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
@@ -171,6 +156,12 @@ impl Sql {
|
||||
.context("failed to vacuum the database")?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE")?;
|
||||
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
@@ -340,16 +331,24 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Execute the given query, returning the number of affected rows.
|
||||
pub async fn execute(&self, query: &str, params: impl rusqlite::Params) -> Result<usize> {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> Result<usize> {
|
||||
let conn = self.get_conn().await?;
|
||||
let res = conn.execute(query, params)?;
|
||||
let res = conn.execute(query.as_ref(), params)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Executes the given query, returning the last inserted row ID.
|
||||
pub async fn insert(&self, query: &str, params: impl rusqlite::Params) -> Result<i64> {
|
||||
pub async fn insert(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> Result<i64> {
|
||||
let conn = self.get_conn().await?;
|
||||
conn.execute(query, params)?;
|
||||
conn.execute(query.as_ref(), params)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
@@ -358,7 +357,7 @@ impl Sql {
|
||||
/// result of that function.
|
||||
pub async fn query_map<T, F, G, H>(
|
||||
&self,
|
||||
sql: &str,
|
||||
sql: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
mut g: G,
|
||||
@@ -367,6 +366,8 @@ impl Sql {
|
||||
F: FnMut(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
G: FnMut(rusqlite::MappedRows<F>) -> Result<H>,
|
||||
{
|
||||
let sql = sql.as_ref();
|
||||
|
||||
let conn = self.get_conn().await?;
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let res = stmt.query_map(params, f)?;
|
||||
@@ -384,7 +385,11 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
|
||||
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> anyhow::Result<usize> {
|
||||
pub async fn count(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> anyhow::Result<usize> {
|
||||
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
|
||||
Ok(usize::try_from(count)?)
|
||||
}
|
||||
@@ -399,7 +404,7 @@ impl Sql {
|
||||
/// Execute a query which is expected to return one row.
|
||||
pub async fn query_row<T, F>(
|
||||
&self,
|
||||
query: &str,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
) -> Result<T>
|
||||
@@ -407,7 +412,7 @@ impl Sql {
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
{
|
||||
let conn = self.get_conn().await?;
|
||||
let res = conn.query_row(query, params, f)?;
|
||||
let res = conn.query_row(query.as_ref(), params, f)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -469,7 +474,7 @@ impl Sql {
|
||||
/// Execute a query which is expected to return zero or one row.
|
||||
pub async fn query_row_optional<T, F>(
|
||||
&self,
|
||||
sql: &str,
|
||||
sql: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
) -> anyhow::Result<Option<T>>
|
||||
@@ -711,15 +716,13 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
Ok(mut dir_handle) => {
|
||||
/* avoid deletion of files that are just created to build a message object */
|
||||
let diff = std::time::Duration::from_secs(60 * 60);
|
||||
let keep_files_newer_than = std::time::SystemTime::now()
|
||||
.checked_sub(diff)
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
|
||||
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => break,
|
||||
};
|
||||
if entry.is_err() {
|
||||
break;
|
||||
}
|
||||
let entry = entry.unwrap();
|
||||
let name_f = entry.file_name();
|
||||
let name_s = name_f.to_string_lossy();
|
||||
|
||||
@@ -735,13 +738,11 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
|
||||
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
|
||||
let recently_created =
|
||||
stats.created().map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_modified = stats
|
||||
.modified()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_accessed = stats
|
||||
.accessed()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
|
||||
let recently_modified = stats.modified().is_ok()
|
||||
&& stats.modified().unwrap() > keep_files_newer_than;
|
||||
let recently_accessed = stats.accessed().is_ok()
|
||||
&& stats.accessed().unwrap() > keep_files_newer_than;
|
||||
|
||||
if recently_created || recently_modified || recently_accessed {
|
||||
info!(
|
||||
|
||||
42
src/sync.rs
42
src/sync.rs
@@ -199,7 +199,7 @@ impl Context {
|
||||
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
|
||||
self.sql
|
||||
.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||
format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
@@ -208,7 +208,7 @@ impl Context {
|
||||
|
||||
/// Takes a JSON string created by `build_sync_json()`
|
||||
/// and construct `SyncItems` from it.
|
||||
pub(crate) fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
|
||||
pub(crate) async fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
|
||||
let sync_items: SyncItems = serde_json::from_str(&serialized)?;
|
||||
Ok(sync_items)
|
||||
}
|
||||
@@ -317,7 +317,7 @@ mod tests {
|
||||
t.delete_sync_ids(ids).await?;
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized)?;
|
||||
let sync_items = t.parse_sync_items(serialized).await?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
|
||||
Ok(())
|
||||
@@ -341,44 +341,56 @@ mod tests {
|
||||
async fn test_parse_sync_items() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert!(t.parse_sync_items(r#"{bad json}"#.to_string()).is_err());
|
||||
assert!(t
|
||||
.parse_sync_items(r#"{bad json}"#.to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
|
||||
assert!(t
|
||||
.parse_sync_items(r#"{"badname":[]}"#.to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.is_err());
|
||||
.await.is_err());
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `123` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `true` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `[]` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // `{}` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
|
||||
)
|
||||
.await
|
||||
.is_err()); // missing field
|
||||
|
||||
// empty item list is okay
|
||||
assert_eq!(
|
||||
t.parse_sync_items(r#"{"items":[]}"#.to_string())?
|
||||
t.parse_sync_items(r#"{"items":[]}"#.to_string())
|
||||
.await?
|
||||
.items
|
||||
.len(),
|
||||
0
|
||||
@@ -393,15 +405,17 @@ mod tests {
|
||||
]}"#
|
||||
.to_string(),
|
||||
)
|
||||
?;
|
||||
.await?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
|
||||
],"additional":"field"}"#
|
||||
.to_string(),
|
||||
)?;
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
|
||||
@@ -416,7 +430,7 @@ mod tests {
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
|
||||
)
|
||||
?;
|
||||
.await?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
|
||||
Ok(())
|
||||
@@ -440,7 +454,7 @@ mod tests {
|
||||
]}"#
|
||||
.to_string(),
|
||||
)
|
||||
?;
|
||||
.await?;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
|
||||
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Utilities to help writing tests.
|
||||
//!
|
||||
//! This private module is only compiled for test runs.
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::panic;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ansi_term::Color;
|
||||
@@ -143,6 +144,8 @@ pub struct TestContext {
|
||||
pub evtracker: EventTracker,
|
||||
/// Channels which should receive events from this context.
|
||||
event_senders: Arc<RwLock<Vec<Sender<Event>>>>,
|
||||
/// Receives panics from sinks ("sink" means "event handler" here)
|
||||
poison_receiver: Receiver<String>,
|
||||
/// Reference to implicit [`LogSink`] so it is dropped together with the context.
|
||||
///
|
||||
/// Only used if no explicit `log_sender` is passed into [`TestContext::new_internal`]
|
||||
@@ -227,13 +230,30 @@ impl TestContext {
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
let event_senders = Arc::new(RwLock::new(vec![log_sender, evtracker_sender]));
|
||||
let senders = Arc::clone(&event_senders);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
|
||||
task::spawn(async move {
|
||||
// Make sure that the test fails if there is a panic on this thread here
|
||||
// (but not if there is a panic on another thread)
|
||||
let looptask_id = task::current().id();
|
||||
let orig_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Some(panicked_task) = task::try_current() {
|
||||
if panicked_task.id() == looptask_id {
|
||||
poison_sender.try_send(panic_info.to_string()).ok();
|
||||
}
|
||||
}
|
||||
orig_hook(panic_info);
|
||||
}));
|
||||
|
||||
while let Some(event) = events.recv().await {
|
||||
for sender in senders.read().await.iter() {
|
||||
// Don't block because someone wanted to use a oneshot receiver, use
|
||||
// an unbounded channel if you want all events.
|
||||
sender.try_send(event.clone()).ok();
|
||||
{
|
||||
let sinks = senders.read().await;
|
||||
for sender in sinks.iter() {
|
||||
// Don't block because someone wanted to use a oneshot receiver, use
|
||||
// an unbounded channel if you want all events.
|
||||
sender.try_send(event.clone()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -243,6 +263,7 @@ impl TestContext {
|
||||
dir,
|
||||
evtracker: EventTracker(evtracker_receiver),
|
||||
event_senders,
|
||||
poison_receiver,
|
||||
log_sink,
|
||||
}
|
||||
}
|
||||
@@ -349,41 +370,17 @@ impl TestContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Receive a message using the `dc_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 {
|
||||
let received = self.recv_msg_opt(msg).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
received.msg_ids.len(),
|
||||
1,
|
||||
"recv_msg() can currently only receive messages with exactly one part"
|
||||
);
|
||||
let msg = Message::load_from_db(self, received.msg_ids[0])
|
||||
/// Receive a message.
|
||||
///
|
||||
/// Receives a message using the `dc_receive_imf()` pipeline.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) {
|
||||
let received_msg =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
|
||||
.to_owned()
|
||||
+ msg.payload();
|
||||
dc_receive_imf(&self.ctx, received_msg.as_bytes(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat_msgs = chat::get_chat_msgs(self, received.chat_id, 0)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
chat_msgs.contains(&ChatItem::Message { msg_id: msg.id }),
|
||||
"received message is not shown in chat, maybe it's hidden (you may have \
|
||||
to call set_config(Config::ShowEmails, Some(\"2\")).await)"
|
||||
);
|
||||
|
||||
msg
|
||||
}
|
||||
|
||||
/// Receive a message using the `dc_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::dc_receive_imf::ReceivedMsg> {
|
||||
dc_receive_imf(self, msg.payload().as_bytes(), false)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Gets the most recent message of a chat.
|
||||
@@ -596,6 +593,16 @@ impl Deref for TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
if !thread::panicking() {
|
||||
if let Ok(p) = self.poison_receiver.try_recv() {
|
||||
panic!("{}", p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A receiver of [`Event`]s which will log the events to the captured test stdout.
|
||||
///
|
||||
/// Tests redirect the stdout of the test thread and capture this, showing the captured
|
||||
|
||||
434
src/webxdc.rs
434
src/webxdc.rs
@@ -1,13 +1,11 @@
|
||||
//! # Handle webxdc messages.
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_create_smeared_timestamp, dc_open_file_std};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::param::Params;
|
||||
use crate::{chat, EventType};
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use async_std::path::PathBuf;
|
||||
@@ -197,41 +195,6 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the last message of a chat is an info message belonging to the given instance and sender.
|
||||
/// If so, the id of this message is returned.
|
||||
async fn get_overwritable_info_msg_id(
|
||||
&self,
|
||||
instance: &Message,
|
||||
from_id: ContactId,
|
||||
) -> Result<Option<MsgId>> {
|
||||
if let Some((last_msg_id, last_from_id, last_param, last_in_repl_to)) = self
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"SELECT id, from_id, param, mime_in_reply_to
|
||||
FROM msgs
|
||||
WHERE chat_id=?1 AND hidden=0
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1"#,
|
||||
paramsv![instance.chat_id],
|
||||
|row| {
|
||||
let last_msg_id: MsgId = row.get(0)?;
|
||||
let last_from_id: ContactId = row.get(1)?;
|
||||
let last_param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
|
||||
let last_in_repl_to: String = row.get(3)?;
|
||||
Ok((last_msg_id, last_from_id, last_param, last_in_repl_to))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if last_from_id == from_id
|
||||
&& last_param.get_cmd() == SystemMessage::WebxdcInfoMessage
|
||||
&& last_in_repl_to == instance.rfc724_mid
|
||||
{
|
||||
return Ok(Some(last_msg_id));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Takes an update-json as `{payload: PAYLOAD}`
|
||||
/// writes it to the database and handles events, info-messages, document name and summary.
|
||||
async fn create_status_update_record(
|
||||
@@ -239,48 +202,41 @@ impl Context {
|
||||
instance: &mut Message,
|
||||
update_str: &str,
|
||||
timestamp: i64,
|
||||
can_info_msg: bool,
|
||||
from_id: ContactId,
|
||||
) -> Result<StatusUpdateSerial> {
|
||||
let update_str = update_str.trim();
|
||||
if update_str.is_empty() {
|
||||
bail!("create_status_update_record: empty update.");
|
||||
}
|
||||
|
||||
let status_update_item: StatusUpdateItem =
|
||||
let status_update_item: StatusUpdateItem = {
|
||||
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(update_str) {
|
||||
item
|
||||
match instance.state {
|
||||
MessageState::Undefined
|
||||
| MessageState::OutPreparing
|
||||
| MessageState::OutDraft => StatusUpdateItem {
|
||||
payload: item.payload,
|
||||
info: None, // no info-messages in draft mode
|
||||
document: item.document,
|
||||
summary: item.summary,
|
||||
},
|
||||
_ => item,
|
||||
}
|
||||
} else {
|
||||
bail!("create_status_update_record: no valid update item.");
|
||||
};
|
||||
|
||||
if can_info_msg {
|
||||
if let Some(ref info) = status_update_item.info {
|
||||
if let Some(info_msg_id) =
|
||||
self.get_overwritable_info_msg_id(instance, from_id).await?
|
||||
{
|
||||
chat::update_msg_text_and_timestamp(
|
||||
self,
|
||||
instance.chat_id,
|
||||
info_msg_id,
|
||||
info.as_str(),
|
||||
timestamp,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
chat::add_info_msg_with_cmd(
|
||||
self,
|
||||
instance.chat_id,
|
||||
info.as_str(),
|
||||
SystemMessage::WebxdcInfoMessage,
|
||||
timestamp,
|
||||
None,
|
||||
Some(instance),
|
||||
Some(from_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref info) = status_update_item.info {
|
||||
chat::add_info_msg_with_cmd(
|
||||
self,
|
||||
instance.chat_id,
|
||||
info.as_str(),
|
||||
SystemMessage::Unknown,
|
||||
timestamp,
|
||||
None,
|
||||
Some(instance),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut param_changed = false;
|
||||
@@ -306,7 +262,7 @@ impl Context {
|
||||
}
|
||||
|
||||
if param_changed {
|
||||
instance.update_param(self).await?;
|
||||
instance.update_param(self).await;
|
||||
self.emit_msgs_changed(instance.chat_id, instance.id);
|
||||
}
|
||||
|
||||
@@ -349,51 +305,46 @@ impl Context {
|
||||
let chat = Chat::load_from_db(self, instance.chat_id).await?;
|
||||
ensure!(chat.can_send(self).await?, "cannot send to {}", chat.id);
|
||||
|
||||
let send_now = !matches!(
|
||||
instance.state,
|
||||
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
|
||||
);
|
||||
|
||||
let status_update_serial = self
|
||||
.create_status_update_record(
|
||||
&mut instance,
|
||||
update_str,
|
||||
dc_create_smeared_timestamp(self).await,
|
||||
send_now,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if send_now {
|
||||
// send update now
|
||||
// (also send updates on MessagesState::Failed, maybe only one member cannot receive)
|
||||
let mut status_update = Message {
|
||||
chat_id: instance.chat_id,
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(descr.to_string()),
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
};
|
||||
status_update
|
||||
.param
|
||||
.set_cmd(SystemMessage::WebxdcStatusUpdate);
|
||||
status_update.param.set(
|
||||
Param::Arg,
|
||||
self.render_webxdc_status_update_object(
|
||||
instance_msg_id,
|
||||
Some(status_update_serial),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Status object expected."))?,
|
||||
);
|
||||
status_update.set_quote(self, Some(&instance)).await?;
|
||||
status_update.param.remove(Param::GuaranteeE2ee); // may be set by set_quote(), if #2985 is done, this line can be removed
|
||||
let status_update_msg_id =
|
||||
chat::send_msg(self, instance.chat_id, &mut status_update).await?;
|
||||
Ok(Some(status_update_msg_id))
|
||||
} else {
|
||||
// send update once the instance is actually send
|
||||
Ok(None)
|
||||
match instance.state {
|
||||
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft => {
|
||||
// send update once the instance is actually send
|
||||
Ok(None)
|
||||
}
|
||||
_ => {
|
||||
// send update now
|
||||
// (also send updates on MessagesState::Failed, maybe only one member cannot receive)
|
||||
let mut status_update = Message {
|
||||
chat_id: instance.chat_id,
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(descr.to_string()),
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
};
|
||||
status_update
|
||||
.param
|
||||
.set_cmd(SystemMessage::WebxdcStatusUpdate);
|
||||
status_update.param.set(
|
||||
Param::Arg,
|
||||
self.render_webxdc_status_update_object(
|
||||
instance_msg_id,
|
||||
Some(status_update_serial),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Status object expected."))?,
|
||||
);
|
||||
status_update.set_quote(self, Some(&instance)).await?;
|
||||
status_update.param.remove(Param::GuaranteeE2ee); // may be set by set_quote(), if #2985 is done, this line can be removed
|
||||
let status_update_msg_id =
|
||||
chat::send_msg(self, instance.chat_id, &mut status_update).await?;
|
||||
Ok(Some(status_update_msg_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,25 +361,18 @@ impl Context {
|
||||
/// Receives status updates from receive_imf to the database
|
||||
/// and sends out an event.
|
||||
///
|
||||
/// `from_id` is the sender
|
||||
///
|
||||
/// `msg_id` may be an instance (in case there are initial status updates)
|
||||
/// or a reply to an instance (for all other updates).
|
||||
///
|
||||
/// `json` is an array containing one or more update items as created by send_webxdc_status_update(),
|
||||
/// the array is parsed using serde, the single payloads are used as is.
|
||||
pub(crate) async fn receive_status_update(
|
||||
&self,
|
||||
from_id: ContactId,
|
||||
msg_id: MsgId,
|
||||
json: &str,
|
||||
) -> Result<()> {
|
||||
pub(crate) async fn receive_status_update(&self, msg_id: MsgId, json: &str) -> Result<()> {
|
||||
let msg = Message::load_from_db(self, msg_id).await?;
|
||||
let (timestamp, mut instance, can_info_msg) = if msg.viewtype == Viewtype::Webxdc {
|
||||
(msg.timestamp_sort, msg, false)
|
||||
let (timestamp, mut instance) = if msg.viewtype == Viewtype::Webxdc {
|
||||
(msg.timestamp_sort, msg)
|
||||
} else if let Some(parent) = msg.parent(self).await? {
|
||||
if parent.viewtype == Viewtype::Webxdc {
|
||||
(msg.timestamp_sort, parent, true)
|
||||
(msg.timestamp_sort, parent)
|
||||
} else {
|
||||
bail!("receive_status_update: message is not the child of a webxdc message.")
|
||||
}
|
||||
@@ -442,8 +386,6 @@ impl Context {
|
||||
&mut instance,
|
||||
&*serde_json::to_string(&update_item)?,
|
||||
timestamp,
|
||||
can_info_msg,
|
||||
from_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -547,12 +489,12 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
||||
async fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
||||
let manifest: WebxdcManifest = toml::from_slice(bytes)?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
fn get_blob(archive: &mut ZipArchive<File>, name: &str) -> Result<Vec<u8>> {
|
||||
async fn get_blob(archive: &mut ZipArchive<File>, name: &str) -> Result<Vec<u8>> {
|
||||
let mut file = archive.by_name(name)?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
@@ -591,8 +533,8 @@ impl Message {
|
||||
let mut archive = self.get_webxdc_archive(context).await?;
|
||||
|
||||
if name == "index.html" {
|
||||
if let Ok(bytes) = get_blob(&mut archive, "manifest.toml") {
|
||||
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
|
||||
if let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await {
|
||||
if let Ok(manifest) = parse_webxdc_manifest(&bytes).await {
|
||||
if let Some(min_api) = manifest.min_api {
|
||||
if min_api > WEBXDC_API_VERSION {
|
||||
return Ok(Vec::from(
|
||||
@@ -604,7 +546,7 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
get_blob(&mut archive, name)
|
||||
get_blob(&mut archive, name).await
|
||||
}
|
||||
|
||||
/// Return info from manifest.toml or from fallbacks.
|
||||
@@ -612,8 +554,8 @@ impl Message {
|
||||
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
|
||||
let mut archive = self.get_webxdc_archive(context).await?;
|
||||
|
||||
let mut manifest = if let Ok(bytes) = get_blob(&mut archive, "manifest.toml") {
|
||||
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
|
||||
let mut manifest = if let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await {
|
||||
if let Ok(manifest) = parse_webxdc_manifest(&bytes).await {
|
||||
manifest
|
||||
} else {
|
||||
WebxdcManifest {
|
||||
@@ -678,11 +620,10 @@ mod tests {
|
||||
use async_std::io::WriteExt;
|
||||
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, forward_msgs, resend_msgs, send_msg, send_text_msg,
|
||||
ChatId, ProtectionStatus,
|
||||
add_contact_to_chat, create_group_chat, forward_msgs, send_msg, send_text_msg, ChatId,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::contact::Contact;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
@@ -859,51 +800,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_resend_webxdc_instance_and_info() -> Result<()> {
|
||||
// Alice uses webxdc in a group
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_grp).await?;
|
||||
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 1);
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
alice_instance.id,
|
||||
r#"{"payload":7,"info": "i","summary":"s"}"#,
|
||||
"d",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2);
|
||||
assert!(alice.get_last_msg_in(alice_grp).await.is_info());
|
||||
|
||||
// Alice adds Bob and resend already used webxdc
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3);
|
||||
resend_msgs(&alice, &[alice_instance.id]).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob received webxdc, legacy info-messages updates are received but not added to the chat
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert!(!bob_instance.is_info());
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"#
|
||||
);
|
||||
let bob_grp = bob_instance.chat_id;
|
||||
assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id);
|
||||
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_receive_webxdc_instance() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -1029,8 +925,6 @@ mod tests {
|
||||
&mut instance,
|
||||
"\n\n{\"payload\": {\"foo\":\"bar\"}}\n",
|
||||
1640178619,
|
||||
true,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
@@ -1040,17 +934,11 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(t
|
||||
.create_status_update_record(&mut instance, "\n\n\n", 1640178619, true, ContactId::SELF)
|
||||
.create_status_update_record(&mut instance, "\n\n\n", 1640178619)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(t
|
||||
.create_status_update_record(
|
||||
&mut instance,
|
||||
"bad json",
|
||||
1640178619,
|
||||
true,
|
||||
ContactId::SELF
|
||||
)
|
||||
.create_status_update_record(&mut instance, "bad json", 1640178619)
|
||||
.await
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
@@ -1064,22 +952,14 @@ mod tests {
|
||||
&mut instance,
|
||||
r#"{"payload" : { "foo2":"bar2"}}"#,
|
||||
1640178619,
|
||||
true,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_webxdc_status_updates(instance.id, update_id1).await?,
|
||||
r#"[{"payload":{"foo2":"bar2"},"serial":2,"max_serial":2}]"#
|
||||
);
|
||||
t.create_status_update_record(
|
||||
&mut instance,
|
||||
r#"{"payload":true}"#,
|
||||
1640178619,
|
||||
true,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await?;
|
||||
t.create_status_update_record(&mut instance, r#"{"payload":true}"#, 1640178619)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
@@ -1093,8 +973,6 @@ mod tests {
|
||||
&mut instance,
|
||||
r#"{"payload" : 1, "sender": "that is not used"}"#,
|
||||
1640178619,
|
||||
true,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
@@ -1113,40 +991,24 @@ mod tests {
|
||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||
|
||||
assert!(t
|
||||
.receive_status_update(ContactId::SELF, instance.id, r#"foo: bar"#)
|
||||
.receive_status_update(instance.id, r#"foo: bar"#)
|
||||
.await
|
||||
.is_err()); // no json
|
||||
assert!(t
|
||||
.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#"{"updada":[{"payload":{"foo":"bar"}}]}"#
|
||||
)
|
||||
.receive_status_update(instance.id, r#"{"updada":[{"payload":{"foo":"bar"}}]}"#)
|
||||
.await
|
||||
.is_err()); // "updates" object missing
|
||||
assert!(t
|
||||
.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#"{"updates":[{"foo":"bar"}]}"#
|
||||
)
|
||||
.receive_status_update(instance.id, r#"{"updates":[{"foo":"bar"}]}"#)
|
||||
.await
|
||||
.is_err()); // "payload" field missing
|
||||
assert!(t
|
||||
.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#"{"updates":{"payload":{"foo":"bar"}}}"#
|
||||
)
|
||||
.receive_status_update(instance.id, r#"{"updates":{"payload":{"foo":"bar"}}}"#)
|
||||
.await
|
||||
.is_err()); // not an array
|
||||
|
||||
t.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#"{"updates":[{"payload":{"foo":"bar"}}]}"#,
|
||||
)
|
||||
.await?;
|
||||
t.receive_status_update(instance.id, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
@@ -1154,7 +1016,6 @@ mod tests {
|
||||
);
|
||||
|
||||
t.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#" {"updates": [ {"payload" :42} , {"payload": 23} ] } "#,
|
||||
)
|
||||
@@ -1168,7 +1029,6 @@ mod tests {
|
||||
);
|
||||
|
||||
t.receive_status_update(
|
||||
ContactId::SELF,
|
||||
instance.id,
|
||||
r#" {"updates": [ {"payload" :"ok", "future_item": "test"} ], "from": "future" } "#,
|
||||
)
|
||||
@@ -1205,7 +1065,6 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_send_webxdc_status_update() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.set_config_bool(Config::BccSelf, true).await?;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends an webxdc instance and a status update
|
||||
@@ -1262,7 +1121,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Bob receives all messages
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
@@ -1287,19 +1147,6 @@ mod tests {
|
||||
assert_eq!(alice2_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 1);
|
||||
|
||||
// To support the second device, Alice has enabled bcc_self and will receive their own messages;
|
||||
// these messages, however, should be ignored
|
||||
alice.recv_msg_opt(sent1).await;
|
||||
alice.recv_msg_opt(sent2).await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(
|
||||
alice
|
||||
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
|
||||
{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1372,7 +1219,8 @@ mod tests {
|
||||
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
||||
|
||||
// bob receives the instance together with the initial updates in a single message
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(bob_instance.get_filename(), Some("minimal.xdc".to_string()));
|
||||
assert!(sent1.payload().contains("Content-Type: application/json"));
|
||||
@@ -1382,9 +1230,8 @@ mod tests {
|
||||
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
|
||||
{"payload":42,"info":"i","serial":2,"max_serial":2}]"#
|
||||
{"payload":42,"serial":2,"max_serial":2}]"# // 'info: "i"' ignored as sent in draft mode
|
||||
);
|
||||
assert!(!bob.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1486,20 +1333,21 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webxdc_manifest() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes()).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes()).await?;
|
||||
assert_eq!(manifest.name, None);
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes()).await?;
|
||||
assert_eq!(manifest.name, Some("name, no icon".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foo"
|
||||
icon = "bar""#
|
||||
.as_bytes(),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(manifest.name, Some("foo".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
@@ -1510,20 +1358,21 @@ add_item = "that should be just ignored"
|
||||
[section]
|
||||
sth_for_the = "future""#
|
||||
.as_bytes(),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(manifest.name, Some("foz".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes()).await?;
|
||||
assert_eq!(manifest.min_api, Some(3));
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
|
||||
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes()).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes()).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -1531,10 +1380,11 @@ sth_for_the = "future""#
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes()).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
|
||||
let manifest =
|
||||
parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes()).await?;
|
||||
assert_eq!(
|
||||
manifest.source_code_url,
|
||||
Some("https://foo.bar".to_string())
|
||||
@@ -1687,7 +1537,8 @@ sth_for_the = "future""#
|
||||
assert_eq!(info.summary, "sum: 2".to_string());
|
||||
|
||||
// Bob receives the updates
|
||||
let bob_instance = bob.recv_msg(sent_instance).await;
|
||||
bob.recv_msg(sent_instance).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
bob.recv_msg(sent_update1).await;
|
||||
bob.recv_msg(sent_update2).await;
|
||||
let info = Message::load_from_db(&bob, bob_instance.id)
|
||||
@@ -1698,7 +1549,8 @@ sth_for_the = "future""#
|
||||
|
||||
// Alice has a second device and also receives the updates there
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice2_instance = alice2.recv_msg(sent_instance).await;
|
||||
alice2.recv_msg(sent_instance).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
alice2.recv_msg(sent_update1).await;
|
||||
alice2.recv_msg(sent_update2).await;
|
||||
let info = Message::load_from_db(&alice2, alice2_instance.id)
|
||||
@@ -1739,7 +1591,8 @@ sth_for_the = "future""#
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
|
||||
// Bob receives the updates
|
||||
let bob_instance = bob.recv_msg(sent_instance).await;
|
||||
bob.recv_msg(sent_instance).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
bob.recv_msg(sent_update1).await;
|
||||
let info = Message::load_from_db(&bob, bob_instance.id)
|
||||
.await?
|
||||
@@ -1773,8 +1626,6 @@ sth_for_the = "future""#
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
assert_eq!(info_msg.from_id, ContactId::SELF);
|
||||
assert_eq!(
|
||||
info_msg.get_text(),
|
||||
Some("this appears in-chat".to_string())
|
||||
@@ -1792,14 +1643,13 @@ sth_for_the = "future""#
|
||||
);
|
||||
|
||||
// Bob receives all messages
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
assert!(!info_msg.from_id.is_special());
|
||||
assert_eq!(
|
||||
info_msg.get_text(),
|
||||
Some("this appears in-chat".to_string())
|
||||
@@ -1814,14 +1664,13 @@ sth_for_the = "future""#
|
||||
|
||||
// Alice has a second device and also receives the info message there
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice2_instance = alice2.recv_msg(sent1).await;
|
||||
alice2.recv_msg(sent1).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
let alice2_chat_id = alice2_instance.chat_id;
|
||||
alice2.recv_msg(sent2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
|
||||
let info_msg = alice2.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
assert_eq!(info_msg.from_id, ContactId::SELF);
|
||||
assert_eq!(
|
||||
info_msg.get_text(),
|
||||
Some("this appears in-chat".to_string())
|
||||
@@ -1841,60 +1690,6 @@ sth_for_the = "future""#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
let sent1 = &alice.pop_sent_msg().await;
|
||||
|
||||
// Alice sends two info messages in a row;
|
||||
// the second one removes the first one as there is nothing in between
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i1", "payload":1}"#, "d")
|
||||
.await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#, "d")
|
||||
.await?;
|
||||
let sent3 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), Some("i2".to_string()));
|
||||
|
||||
// When Bob receives the messages, they should be cleaned up as well
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
bob.recv_msg(sent3).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), Some("i2".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?;
|
||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||
|
||||
t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#, "d")
|
||||
.await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2);
|
||||
send_text_msg(&t, chat_id, "msg between info".to_string()).await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3);
|
||||
t.send_webxdc_status_update(instance.id, r#"{"info":"i2", "payload":2}"#, "d")
|
||||
.await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_opportunistic_encryption() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -1926,7 +1721,8 @@ sth_for_the = "future""#
|
||||
assert!(update_msg.get_showpadlock());
|
||||
|
||||
// Bob receives instance+update
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
bob.recv_msg(sent2).await;
|
||||
assert!(bob_instance.get_showpadlock());
|
||||
|
||||
@@ -1990,12 +1786,14 @@ sth_for_the = "future""#
|
||||
|
||||
// Bob receives that instance
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
assert_eq!(bob_instance.get_text(), Some("user added text".to_string()));
|
||||
|
||||
// Alice's second device receives the instance as well
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice2_instance = alice2.recv_msg(&sent1).await;
|
||||
alice2.recv_msg(&sent1).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
assert_eq!(
|
||||
alice2_instance.get_text(),
|
||||
Some("user added text".to_string())
|
||||
|
||||
Reference in New Issue
Block a user