mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
5 Commits
http-uploa
...
test-ecc-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed092fe1ce | ||
|
|
56f589db4a | ||
|
|
49b39b42bf | ||
|
|
7e4951e691 | ||
|
|
f50874acf1 |
@@ -7,10 +7,6 @@ executors:
|
||||
doxygen:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
python:
|
||||
docker:
|
||||
- image: 3.7.7-stretch
|
||||
|
||||
|
||||
restore-workspace: &restore-workspace
|
||||
attach_workspace:
|
||||
@@ -138,6 +134,12 @@ jobs:
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
remote_tests_rust:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: ci_scripts/remote_tests_rust.sh
|
||||
|
||||
remote_tests_python:
|
||||
machine: true
|
||||
steps:
|
||||
@@ -172,6 +174,11 @@ workflows:
|
||||
jobs:
|
||||
# - cargo_fetch
|
||||
|
||||
- remote_tests_rust:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- remote_tests_python:
|
||||
filters:
|
||||
tags:
|
||||
@@ -180,9 +187,8 @@ workflows:
|
||||
- remote_python_packaging:
|
||||
requires:
|
||||
- remote_tests_python
|
||||
- remote_tests_rust
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
@@ -191,8 +197,6 @@ workflows:
|
||||
- remote_python_packaging
|
||||
- build_doxygen
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
# - rustfmt:
|
||||
@@ -204,8 +208,6 @@ workflows:
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
|
||||
89
.github/workflows/ci.yml
vendored
89
.github/workflows/ci.yml
vendored
@@ -1,89 +0,0 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- staging
|
||||
- trying
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.43.1
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.43.1
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [nightly, 1.43.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Install ${{ matrix.rust }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --workspace --all --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace
|
||||
|
||||
48
.github/workflows/code-quality.yml
vendored
Normal file
48
.github/workflows/code-quality.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
on: push
|
||||
name: Code Quality
|
||||
jobs:
|
||||
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2019-11-06
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --workspace --examples --tests --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2019-11-06
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-2019-11-06
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
|
||||
|
||||
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
|
||||
|
||||
APIs:
|
||||
dc_context_new # opens the database
|
||||
dc_open # FFI only
|
||||
-> drop it and move parameters to dc_context_new()
|
||||
|
||||
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
|
||||
dc_imex NEVER goes through the job system
|
||||
dc_imex import_backup needs to ensure dc_stop_jobs()
|
||||
|
||||
dc_start_io # start smtp/imap and job handling subsystems
|
||||
dc_stop_io # stop smtp/imap and job handling subsystems
|
||||
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
|
||||
|
||||
dc_close # FFI only
|
||||
-> can be dropped
|
||||
dc_context_unref
|
||||
|
||||
for ios share-extension:
|
||||
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
|
||||
0: message was sent
|
||||
1: message failed to go out, is queued as a job to be retried later
|
||||
2: message permanently failed?
|
||||
|
||||
EVENT handling:
|
||||
start a callback thread and call get_next_event() which is BLOCKING
|
||||
it's fine to start this callback thread later, it will see all events.
|
||||
Note that the core infinitely fills the internal queue if you never drain it.
|
||||
|
||||
FFI-get_next_event() returns NULL if the context is unrefed already?
|
||||
|
||||
sidenote: how python's callback thread does it currently:
|
||||
CB-thread runs this while loop:
|
||||
while not QUITFLAG:
|
||||
ev = context.get_next_event( )
|
||||
...
|
||||
So in order to shutdown properly one has to set QUITFLAG
|
||||
before calling dc_stop_jobs() and dc_context_unref
|
||||
|
||||
event API:
|
||||
get_data1_int
|
||||
get_data2_int
|
||||
get_data3_str
|
||||
|
||||
|
||||
- userdata likely only used for the callbacks, likely can be dropped, needs verification
|
||||
|
||||
|
||||
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going
|
||||
|
||||
143
CHANGELOG.md
143
CHANGELOG.md
@@ -1,148 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 1.35.0
|
||||
|
||||
- enable strict-tls from a new provider-db setting #1587
|
||||
|
||||
- new subject 'Message from USER' for one-to-one chats #1395
|
||||
|
||||
- recode images #1563
|
||||
|
||||
- improve reconnect handling #1549 #1580
|
||||
|
||||
- improve importing addresses #1544
|
||||
|
||||
- improve configure and folder detection #1539 #1548
|
||||
|
||||
- improve test suite #1559 #1564 #1580 #1581 #1582 #1584 #1588:
|
||||
|
||||
- fix ad-hoc groups #1566
|
||||
|
||||
- preventions against being marked as spam #1575
|
||||
|
||||
- refactorings #1542 #1569
|
||||
|
||||
|
||||
## 1.34.0
|
||||
|
||||
- new api for io, thread and event handling #1356,
|
||||
see the example atop of `deltachat.h` to get an overview
|
||||
|
||||
- LOTS of speed improvements due to async processing #1356
|
||||
|
||||
- enable WAL mode for sqlite #1492
|
||||
|
||||
- process incoming messages in bulk #1527
|
||||
|
||||
- improve finding out the sent-folder #1488
|
||||
|
||||
- several bug fixes
|
||||
|
||||
|
||||
## 1.33.0
|
||||
|
||||
- let `dc_set_muted()` also mute one-to-one chats #1470
|
||||
|
||||
- fix a bug that led to load and traffic if the server does not use sent-folder
|
||||
#1472
|
||||
|
||||
|
||||
## 1.32.0
|
||||
|
||||
- fix endless loop when trying to download messages with bad RFC Message-ID,
|
||||
also be more reliable on similar errors #1463 #1466 #1462
|
||||
|
||||
- fix bug with comma in contact request #1438
|
||||
|
||||
- do not refer to hidden messages on replies #1459
|
||||
|
||||
- improve error handling #1468 #1465 #1464
|
||||
|
||||
|
||||
## 1.31.0
|
||||
|
||||
- always describe the context of the displayed error #1451
|
||||
|
||||
- do not emit `DC_EVENT_ERROR` when message sending fails;
|
||||
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
|
||||
|
||||
- new config-option `media_quality` #1449
|
||||
|
||||
- try over if writing message to database fails #1447
|
||||
|
||||
|
||||
## 1.30.0
|
||||
|
||||
- expunge deleted messages #1440
|
||||
|
||||
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
|
||||
|
||||
|
||||
## 1.29.0
|
||||
|
||||
- new config options `delete_device_after` and `delete_server_after`,
|
||||
each taking an amount of seconds after which messages
|
||||
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
|
||||
|
||||
- new api `dc_estimate_deletion_cnt()` to estimate the effect
|
||||
of `delete_device_after` and `delete_server_after`
|
||||
|
||||
- use Ed25519 keys by default, these keys are much shorter
|
||||
than RSA keys, which results in saving traffic and speed improvements #1362
|
||||
|
||||
- improve message ellipsizing #1397 #1430
|
||||
|
||||
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
|
||||
|
||||
- do not show badly formatted non-delta-messages as empty #1384
|
||||
|
||||
- try over SMTP on potentially recoverable error 5.5.0 #1379
|
||||
|
||||
- remove device-chat from forward-to-chat-list #1367
|
||||
|
||||
- improve group-handling #1368
|
||||
|
||||
- `dc_get_info()` returns uptime (how long the context is in use)
|
||||
|
||||
- python improvements and adaptions #1408 #1415
|
||||
|
||||
- log to the stdout and stderr in tests #1416
|
||||
|
||||
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
|
||||
|
||||
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
|
||||
|
||||
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
|
||||
|
||||
|
||||
## 1.28.0
|
||||
|
||||
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
||||
that will sort the "saved messages" chat to the top of the chatlist #1336
|
||||
- mark mails as being deleted from server in dc_empty_server() #1333
|
||||
- fix interaction with servers that do not allow folder creation on root-level;
|
||||
use path separator as defined by the email server #1359
|
||||
- fix group creation if group was created by non-delta clients #1357
|
||||
- fix showing replies from non-delta clients #1353
|
||||
- fix member list on rejoining left groups #1343
|
||||
- fix crash when using empty groups #1354
|
||||
- fix potential crash on special names #1350
|
||||
|
||||
|
||||
## 1.27.0
|
||||
|
||||
- handle keys reliably on armv7 #1327
|
||||
|
||||
|
||||
## 1.26.0
|
||||
|
||||
- change generated key type back to RSA as shipped versions
|
||||
have problems to encrypt to Ed25519 keys
|
||||
|
||||
- update rPGP to encrypt reliably to Ed25519 keys;
|
||||
one of the next versions can finally use Ed25519 keys then
|
||||
|
||||
|
||||
## 1.25.0
|
||||
|
||||
- save traffic by downloading only messages that are really displayed #1236
|
||||
|
||||
3389
Cargo.lock
generated
3389
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
66
Cargo.toml
66
Cargo.toml
@@ -1,78 +1,71 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.35.0"
|
||||
version = "1.25.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[profile.release]
|
||||
# lto = true
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { version = "0.6.0", default-features = false }
|
||||
pgp = { version = "0.4.0", default-features = false }
|
||||
hex = "0.4.0"
|
||||
sha2 = "0.9.0"
|
||||
sha2 = "0.8.0"
|
||||
rand = "0.7.0"
|
||||
smallvec = "1.0.0"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
async-smtp = { version = "0.3" }
|
||||
async-smtp = "0.2"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
async-imap = "0.3.1"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-std = { version = "1.6.1", features = ["unstable"] }
|
||||
base64 = "0.12"
|
||||
async-imap = "0.2"
|
||||
async-native-tls = "0.3.1"
|
||||
async-std = { version = "1.4", features = ["unstable"] }
|
||||
base64 = "0.11"
|
||||
charset = "0.1"
|
||||
percent-encoding = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
failure = "0.1.5"
|
||||
failure_derive = "0.1.5"
|
||||
indexmap = "1.3.0"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.23", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.16.0"
|
||||
rusqlite = { version = "0.21", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.13.0"
|
||||
r2d2 = "0.8.5"
|
||||
strum = "0.18.0"
|
||||
strum_macros = "0.18.0"
|
||||
strum = "0.16.0"
|
||||
strum_macros = "0.16.0"
|
||||
thread-local-object = "0.1.0"
|
||||
backtrace = "0.3.33"
|
||||
byteorder = "1.3.1"
|
||||
itertools = "0.8.0"
|
||||
image-meta = "0.1.0"
|
||||
quick-xml = "0.18.1"
|
||||
quick-xml = "0.17.1"
|
||||
escaper = "0.1.0"
|
||||
bitflags = "1.1.0"
|
||||
debug_stub_derive = "0.3.0"
|
||||
sanitize-filename = "0.2.1"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.12.1"
|
||||
mailparse = "0.10.2"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
futures = "0.3.4"
|
||||
thiserror = "1.0.14"
|
||||
anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||
pretty_env_logger = "0.3.1"
|
||||
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
open = { version = "1.4.0", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
pretty_assertions = "0.6.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
|
||||
smol = "0.1.10"
|
||||
pretty_env_logger = "0.3.0"
|
||||
proptest = "0.9.4"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -83,18 +76,15 @@ members = [
|
||||
[[example]]
|
||||
name = "simple"
|
||||
path = "examples/simple.rs"
|
||||
required-features = ["repl"]
|
||||
|
||||
[[example]]
|
||||
name = "repl"
|
||||
path = "examples/repl/main.rs"
|
||||
required-features = ["repl"]
|
||||
required-features = ["rustyline"]
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "open"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
default = ["nightly", "ringbuf"]
|
||||
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
ringbuf = ["pgp/ringbuf"]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
||||
|
||||
```
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
```
|
||||
|
||||
## Using the CLI client
|
||||
@@ -17,9 +17,8 @@ $ curl https://sh.rustup.rs -sSf | sh
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
|
||||
```
|
||||
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||
cargo run --example repl -- /path/to/db
|
||||
```
|
||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||
|
||||
Configure your account (if not already configured):
|
||||
|
||||
@@ -109,6 +108,7 @@ $ cargo test -- --ignored
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
|
||||
|
||||
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
||||
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
|
||||
@@ -132,5 +132,4 @@ or its language bindings:
|
||||
- [iOS](https://github.com/deltachat/deltachat-ios)
|
||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- several **Bots**
|
||||
|
||||
19
appveyor.yml
Normal file
19
appveyor.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
environment:
|
||||
matrix:
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
|
||||
install:
|
||||
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||
- rustup-init -yv --default-toolchain nightly-2019-07-10
|
||||
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
- cargo test --release --all
|
||||
|
||||
cache:
|
||||
- target
|
||||
- C:\Users\appveyor\.cargo\registry
|
||||
@@ -37,7 +37,7 @@ echo -----------------------
|
||||
# Bundle external shared libraries into the wheels
|
||||
pushd $WHEELHOUSEDIR
|
||||
|
||||
pip3 install -U pip setuptools
|
||||
pip3 install -U pip
|
||||
pip3 install devpi-client
|
||||
devpi use https://m.devpi.net
|
||||
devpi login dc --password $DEVPI_LOGIN
|
||||
|
||||
@@ -48,7 +48,7 @@ def run():
|
||||
projectnames = get_projectnames(baseurl, username, indexname)
|
||||
if indexname == "master" or not indexname:
|
||||
continue
|
||||
clear_index = not projectnames
|
||||
assert projectnames == ["deltachat"]
|
||||
for projectname in projectnames:
|
||||
dates = get_release_dates(baseurl, username, indexname, projectname)
|
||||
if not dates:
|
||||
@@ -60,11 +60,8 @@ def run():
|
||||
date = datetime.datetime(*max(dates))
|
||||
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
||||
assert username and indexname
|
||||
clear_index = True
|
||||
break
|
||||
if clear_index:
|
||||
url = baseurl + username + "/" + indexname
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
url = baseurl + username + "/" + indexname
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM quay.io/pypa/manylinux2010_x86_64
|
||||
FROM quay.io/pypa/manylinux1_x86_64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-11-06 -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share
|
||||
rm -rf /root/.rustup/toolchains/nightly-2019-11-06-x86_64-unknown-linux-gnu/share/
|
||||
|
||||
@@ -46,7 +46,6 @@ if [ -n "$TESTS" ]; then
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
popd
|
||||
|
||||
@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
|
||||
docker run -e DCC_PY_LIVECONFIG \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
@@ -46,6 +46,6 @@ echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
mkdir -p workspace
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||
|
||||
@@ -30,7 +30,6 @@ ssh $SSHTARGET <<_HERE
|
||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||
export TARGET=release
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
#rm -rf virtualenv venv
|
||||
|
||||
@@ -21,8 +21,7 @@ export DCC_RS_DEV=$(pwd)
|
||||
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.5
|
||||
ln -s /opt/python/cp35-cp35m/bin/python3.5
|
||||
ln -s /opt/python/cp27-cp27m/bin/python2.7
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||
@@ -38,8 +37,7 @@ mkdir -p $TOXWORKDIR
|
||||
# XXX we may switch on some live-tests on for better ensurances
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
unset DCC_PY_LIVECONFIG
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.35.0"
|
||||
version = "1.25.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -19,13 +19,11 @@ deltachat = { path = "../", default-features = false }
|
||||
libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
failure = "0.1.6"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.6.0"
|
||||
anyhow = "1.0.28"
|
||||
thiserror = "1.0.14"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
default = ["vendored", "nightly", "ringbuf"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
nightly = ["deltachat/nightly"]
|
||||
|
||||
ringbuf = ["deltachat/ringbuf"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
use failure::Fail;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
|
||||
@@ -8,7 +9,7 @@ use std::ptr;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,norun
|
||||
/// use crate::string::{dc_strdup, to_string_lossy};
|
||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
||||
/// unsafe {
|
||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
||||
/// let str_a_copy = dc_strdup(str_a);
|
||||
@@ -16,7 +17,7 @@ use std::ptr;
|
||||
/// assert_ne!(str_a, str_a_copy);
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char;
|
||||
if !s.is_null() {
|
||||
ret = libc::strdup(s);
|
||||
@@ -30,13 +31,13 @@ unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
}
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||
pub(crate) enum CStringError {
|
||||
#[derive(Debug, Fail, PartialEq)]
|
||||
pub enum CStringError {
|
||||
/// The string contains an interior null byte
|
||||
#[error("String contains an interior null byte")]
|
||||
#[fail(display = "String contains an interior null byte")]
|
||||
InteriorNullByte,
|
||||
/// The string is not valid Unicode
|
||||
#[error("String is not valid unicode")]
|
||||
#[fail(display = "String is not valid unicode")]
|
||||
NotUnicode,
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ pub(crate) enum CStringError {
|
||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
||||
/// }
|
||||
/// ```
|
||||
pub(crate) trait OsStrExt {
|
||||
pub trait OsStrExt {
|
||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
||||
///
|
||||
/// This is useful to convert e.g. a [std::path::Path] to
|
||||
@@ -130,16 +131,15 @@ fn os_str_to_c_string_unicode(
|
||||
}
|
||||
|
||||
/// Convenience methods/associated functions for working with [CString]
|
||||
trait CStringExt {
|
||||
/// Create a new [CString], best effort
|
||||
///
|
||||
/// This is helps transitioning from unsafe code.
|
||||
pub trait CStringExt {
|
||||
/// Create a new [CString], yolo style
|
||||
///
|
||||
/// Like the [to_string_lossy] this doesn't give up in the face of
|
||||
/// bad input (embedded null bytes in this case) instead it does
|
||||
/// the best it can by stripping the embedded null bytes.
|
||||
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
let mut s = t.into();
|
||||
s.retain(|&c| c != 0);
|
||||
CString::new(s).unwrap_or_default()
|
||||
/// This unwrap the result, panicking when there are embedded NULL
|
||||
/// bytes.
|
||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
CString::new(t).expect("String contains null byte, can not be CString")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ impl CStringExt for CString {}
|
||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
||||
/// and the compiler sometimes allows it in an unsafe way. These
|
||||
/// methods make it more succinct and help you get it right.
|
||||
pub(crate) trait Strdup {
|
||||
pub trait StrExt {
|
||||
/// Allocate a new raw C `*char` version of this string.
|
||||
///
|
||||
/// This allocates a new raw C string which must be freed using
|
||||
@@ -168,44 +168,35 @@ pub(crate) trait Strdup {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Strdup for T {
|
||||
impl<T: AsRef<str>> StrExt for T {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
// We can not implement for AsRef<OsStr> because we already implement
|
||||
// AsRev<str> and this conflicts. So implement for Path directly.
|
||||
impl Strdup for std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
let tmp = CString::yolo(self.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods to turn optional strings into C strings.
|
||||
///
|
||||
/// This is the same as the [Strdup] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [Strdup]
|
||||
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
|
||||
/// This is the same as the [StrExt] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [StrExt]
|
||||
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
|
||||
/// for `AsRef<&str>`.
|
||||
///
|
||||
/// When the [Option] is [Option::Some] this behaves just like
|
||||
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
||||
/// [StrExt::strdup], when it is [Option::None] a null pointer is
|
||||
/// returned.
|
||||
pub(crate) trait OptStrdup {
|
||||
pub trait OptStrExt {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [Strdup::strdup] for details.
|
||||
/// See [StrExt::strdup] for details.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
let tmp = CString::yolo(s.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
@@ -213,7 +204,7 @@ impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
if s.is_null() {
|
||||
return "".into();
|
||||
}
|
||||
@@ -223,7 +214,7 @@ pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
cstr.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
if s.is_null() {
|
||||
return None;
|
||||
}
|
||||
@@ -244,7 +235,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
unsafe {
|
||||
@@ -256,7 +247,7 @@ pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
}
|
||||
|
||||
@@ -363,14 +354,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cstring_new_lossy() {
|
||||
assert!(CString::new("hel\x00lo").is_err());
|
||||
assert!(CString::new(String::from("hel\x00o")).is_err());
|
||||
let r = CString::new("hello").unwrap();
|
||||
assert_eq!(CString::new_lossy("hello"), r);
|
||||
assert_eq!(CString::new_lossy("hel\x00lo"), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hello")), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
|
||||
fn test_cstring_yolo() {
|
||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,6 +3,7 @@ extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn;
|
||||
|
||||
// For now, assume (not check) that these macroses are applied to enum without
|
||||
// data. If this assumption is violated, compiler error will point to
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
|
||||
Problem: missing eventual group consistency
|
||||
--------------------------------------------
|
||||
|
||||
If group members are concurrently adding new members,
|
||||
the new members will miss each other's additions, example:
|
||||
|
||||
- Alice and Bob are in a two-member group
|
||||
|
||||
- Alice adds Carol, concurrently Bob adds Doris
|
||||
|
||||
- Carol will see a three-member group (Alice, Bob, Carol),
|
||||
Doris will see a different three-member group (Alice, Bob, Doris),
|
||||
and only Alice and Bob will have all four members.
|
||||
|
||||
Note that for verified groups any mitigation mechanism likely
|
||||
needs to make all clients to know who originally added a member.
|
||||
|
||||
|
||||
solution: memorize+attach (possible encrypted) chat-meta mime messages
|
||||
----------------------------------------------------------------------
|
||||
|
||||
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
|
||||
|
||||
|
||||
- All Chat-Group-Member-Added/Removed messages are recorded in their
|
||||
full raw (signed and encrypted) mime-format in the DB
|
||||
|
||||
- If an incoming member-add/member-delete messages has a member list
|
||||
which is, apart from the added/removed member, not consistent
|
||||
with our own view, broadcast a "Chat-Group-Member-Correction" message to
|
||||
all members, attaching the original added/removed mime-message for all mismatching
|
||||
contacts. If we have no relevant add/del information, don't send a
|
||||
correction message out.
|
||||
|
||||
- Upong receiving added/removed attachments we don't do the
|
||||
check_consistency+correction message dance.
|
||||
This avoids recursion problems and hard-to-reason-about chatter.
|
||||
|
||||
Notes:
|
||||
|
||||
- mechanism works for both encrypted and unencrypted add/del messages
|
||||
|
||||
- we already have a "mime_headers" column in the DB for each incoming message.
|
||||
We could extend it to also include the payload and store mime unconditionally
|
||||
for member-added/removed messages.
|
||||
|
||||
- multiple member-added/removed messages can be attached in a single
|
||||
correction message
|
||||
|
||||
- it is minimal on the number of overall messages to reach group consistency
|
||||
(best-case: no extra messages, the ABCD case above: max two extra messages)
|
||||
|
||||
- somewhat backward compatible: older clients will probably ignore
|
||||
messages which are signed by someone who is not the outer From-address.
|
||||
|
||||
- the correction-protocol also helps with dropped messages. If a member
|
||||
did not see a member-added/removed message, the next member add/removed
|
||||
message in the group will likely heal group consistency for this member.
|
||||
|
||||
- we can quite easily extend the mechanism to also provide the group-avatar or
|
||||
other meta-information.
|
||||
|
||||
Discussions of variants
|
||||
++++++++++++++++++++++++
|
||||
|
||||
- instead of acting on MemberAdded/Removed message we could send
|
||||
corrections for any received message that addresses inconsistent group members but
|
||||
a) this would delay group-membership healing
|
||||
b) could lead to a lot of members sending corrections
|
||||
|
||||
- instead of broadcasting correction messages we could only send it to
|
||||
the sender of the inconsistent member-added/removed message.
|
||||
A receiver of such a correction message would then need to forward
|
||||
the message to the members it thinks also have an inconsistent view.
|
||||
This sounds complicated and error-prone. Concretely, if Alice
|
||||
receives Bob's "Member-added: Doris" message, then Alice
|
||||
broadcasting the correction message with "Member-added: Carol"
|
||||
would reach all four members, healing group consistency in one step.
|
||||
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
|
||||
Bob would broadcast a correction message to all four members as well.
|
||||
(Imagine a situation where Alice/Bob added Carol/Doris
|
||||
while both being in an offline or bad-connection situation).
|
||||
|
||||
|
||||
solution2: repeat member-added/removed messages
|
||||
---------------------------------------------------
|
||||
|
||||
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
|
||||
but keep sending out the old headers until the new protocol is sufficiently deployed.
|
||||
|
||||
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
|
||||
which controls repetition of the signed "add/del e-mail address" payload.
|
||||
|
||||
Example::
|
||||
|
||||
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
|
||||
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
|
||||
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
|
||||
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
|
||||
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
|
||||
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
|
||||
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
|
||||
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
|
||||
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
|
||||
=P6GG
|
||||
|
||||
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
|
||||
Receivers will apply the add/del change to the group-membership,
|
||||
decrease the TTL by 1, and if TTL>0 re-sent the header.
|
||||
|
||||
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
|
||||
This allows to propagate, in a cryptographically secured way,
|
||||
who added a member. This is particularly important for allowing
|
||||
to show in verified groups who added a member (planned).
|
||||
|
||||
Disadvantage to solution 1:
|
||||
|
||||
- requires to specify encoding and precise rules for what/how is signed.
|
||||
|
||||
- causes O(N^2) extra messages
|
||||
|
||||
- Not easily extendable for other things (without introducing a new
|
||||
header / encoding)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
@@ -11,6 +10,7 @@ use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::error::Error;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::job::*;
|
||||
use deltachat::location;
|
||||
use deltachat::lot::LotState;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
@@ -23,79 +23,78 @@ use deltachat::{config, provider};
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
|
||||
println!("Resetting tables ({})...", bits);
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
}
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM acpeerstates;",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
}
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM contacts WHERE id>9;",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats WHERE id>9;",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts;",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id>9;",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
params![],
|
||||
)
|
||||
.unwrap();
|
||||
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let data = dc_read_file(context, filename)?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -104,29 +103,38 @@ async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<
|
||||
/// Import a file to the database.
|
||||
/// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on.
|
||||
/// For normal importing, use imex().
|
||||
async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
let mut read_cnt: usize = 0;
|
||||
///
|
||||
/// @private @memberof Context
|
||||
/// @param context The context as created by dc_context_new().
|
||||
/// @param spec The file or directory to import. NULL for the last command.
|
||||
/// @return 1=success, 0=error.
|
||||
fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
|
||||
if !context.sql.is_open() {
|
||||
error!(context, "Import: Database not opened.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut read_cnt = 0;
|
||||
|
||||
let real_spec: String;
|
||||
|
||||
// if `spec` is given, remember it for later usage; if it is not given, try to use the last one
|
||||
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */
|
||||
if let Some(spec) = spec {
|
||||
real_spec = spec.to_string();
|
||||
context
|
||||
.sql()
|
||||
.sql
|
||||
.set_raw_config(context, "import_spec", Some(&real_spec))
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config(context, "import_spec").await;
|
||||
let rs = context.sql.get_raw_config(context, "import_spec");
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
} else {
|
||||
@@ -135,7 +143,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
let dir = std::fs::read_dir(dir_name);
|
||||
if dir.is_err() {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
||||
return false;
|
||||
return 0;
|
||||
} else {
|
||||
let dir = dir.unwrap();
|
||||
for entry in dir {
|
||||
@@ -148,7 +156,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
if name.ends_with(".eml") {
|
||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||
println!("Import: {}", path_plus_name);
|
||||
if poke_eml_file(context, path_plus_name).await.is_ok() {
|
||||
if dc_poke_eml_file(context, path_plus_name).is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
}
|
||||
@@ -157,19 +165,16 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
if read_cnt > 0 {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
}
|
||||
true
|
||||
1
|
||||
}
|
||||
|
||||
async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
let contact = Contact::get_by_id(context, msg.get_from_id())
|
||||
.await
|
||||
.expect("invalid contact");
|
||||
|
||||
fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact");
|
||||
let contact_name = contact.get_name();
|
||||
let contact_id = contact.get_id();
|
||||
|
||||
@@ -212,7 +217,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
);
|
||||
}
|
||||
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
|
||||
fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id.is_daymarker() {
|
||||
@@ -228,8 +233,8 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
|
||||
);
|
||||
lines_out += 1
|
||||
}
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
log_msg(context, "", &msg).await;
|
||||
let msg = Message::load_from_db(context, msg_id)?;
|
||||
log_msg(context, "", &msg);
|
||||
}
|
||||
}
|
||||
if lines_out > 0 {
|
||||
@@ -240,18 +245,18 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
let mut contacts = contacts.to_vec();
|
||||
fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
|
||||
let mut contacts = contacts.clone();
|
||||
if !contacts.contains(&1) {
|
||||
contacts.push(1);
|
||||
}
|
||||
for contact_id in contacts {
|
||||
let line;
|
||||
let mut line2 = "".to_string();
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
let name = contact.get_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_state = contact.is_verified(context).await;
|
||||
let verified_state = contact.is_verified(context);
|
||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
||||
if verified_state == VerifiedStatus::BidirectVerified {
|
||||
" √√"
|
||||
@@ -275,7 +280,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr).await;
|
||||
let peerstate = Peerstate::from_addr(context, &context.sql, &addr);
|
||||
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
@@ -292,9 +297,10 @@ fn chat_prefix(chat: &Chat) -> &'static str {
|
||||
chat.typ.into()
|
||||
}
|
||||
|
||||
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
|
||||
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
|
||||
let mut sel_chat = if !chat_id.is_unset() {
|
||||
Chat::load_from_db(&context, *chat_id).await.ok()
|
||||
Chat::load_from_db(context, chat_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -335,6 +341,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
interrupt\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
help imex (Import/Export)\n\
|
||||
@@ -370,8 +377,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
openfile <msg-id>\n\
|
||||
download <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
markseen <msg-id>\n\
|
||||
@@ -392,14 +397,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
|
||||
clear -- clear screen\n\
|
||||
exit or quit\n\
|
||||
============================================="
|
||||
),
|
||||
},
|
||||
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
|
||||
"initiate-key-transfer" => match initiate_key_transfer(context) {
|
||||
Ok(setup_code) => println!(
|
||||
"Setup code for the transferred setup message: {}",
|
||||
setup_code,
|
||||
@@ -409,9 +413,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let msg_id: MsgId = MsgId::new(arg1.parse()?);
|
||||
let msg = Message::load_from_db(&context, msg_id).await?;
|
||||
let msg = Message::load_from_db(context, msg_id)?;
|
||||
if msg.is_setupmessage() {
|
||||
let setupcodebegin = msg.get_setupcodebegin(&context).await;
|
||||
let setupcodebegin = msg.get_setupcodebegin(context);
|
||||
println!(
|
||||
"The setup code for setup message {} starts with: {}",
|
||||
msg_id,
|
||||
@@ -426,29 +430,29 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
|
||||
continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
has_backup(context, blobdir)?;
|
||||
}
|
||||
"export-backup" => {
|
||||
imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
|
||||
imex(context, ImexMode::ExportBackup, Some(blobdir));
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
|
||||
imex(context, ImexMode::ImportBackup, Some(arg1));
|
||||
}
|
||||
"export-keys" => {
|
||||
imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
|
||||
imex(context, ImexMode::ExportSelfKeys, Some(blobdir));
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
|
||||
imex(context, ImexMode::ImportSelfKeys, Some(blobdir));
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
let setup_code = create_setup_code(context);
|
||||
let file_name = blobdir.join("autocrypt-setup-message.html");
|
||||
let file_content = render_setup_file(&context, &setup_code).await?;
|
||||
async_std::fs::write(&file_name, file_content).await?;
|
||||
let file_content = render_setup_file(context, &setup_code)?;
|
||||
std::fs::write(&file_name, file_content)?;
|
||||
println!(
|
||||
"Setup message written to: {}\nSetup code: {}",
|
||||
file_name.display(),
|
||||
@@ -456,47 +460,49 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
ensure!(0 != dc_reset_tables(context, bits), "Reset failed");
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
context.stop_ongoing();
|
||||
}
|
||||
"set" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let value = if arg2.is_empty() { None } else { Some(arg2) };
|
||||
context.set_config(key, value).await?;
|
||||
context.set_config(key, value)?;
|
||||
}
|
||||
"get" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let val = context.get_config(key).await;
|
||||
let val = context.get_config(key);
|
||||
println!("{}={:?}", key, val);
|
||||
}
|
||||
"info" => {
|
||||
println!("{:#?}", context.get_info().await);
|
||||
println!("{:#?}", context.get_info());
|
||||
}
|
||||
"interrupt" => {
|
||||
interrupt_inbox_idle(context);
|
||||
}
|
||||
"maybenetwork" => {
|
||||
context.maybe_network().await;
|
||||
maybe_network(context);
|
||||
}
|
||||
"housekeeping" => {
|
||||
sql::housekeeping(&context).await;
|
||||
sql::housekeeping(context);
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
let chatlist = Chatlist::try_load(
|
||||
&context,
|
||||
context,
|
||||
listflags,
|
||||
if arg1.is_empty() { None } else { Some(arg1) },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
let cnt = chatlist.len();
|
||||
if cnt > 0 {
|
||||
@@ -505,20 +511,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
chat.get_id().get_fresh_msg_cnt(&context).await,
|
||||
chat.get_id().get_fresh_msg_cnt(context),
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal => "",
|
||||
ChatVisibility::Archived => "📦",
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
);
|
||||
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
|
||||
let lot = chatlist.get_summary(context, i, Some(&chat));
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
" [Archived]"
|
||||
} else {
|
||||
@@ -551,7 +557,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
|
||||
if location::is_sending_locations_to_chat(context, ChatId::new(0)) {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
@@ -561,21 +567,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
bail!("Argument [chat-id] is missing.");
|
||||
}
|
||||
if !arg1.is_empty() {
|
||||
let id = ChatId::new(arg1.parse()?);
|
||||
println!("Selecting chat {}", id);
|
||||
sel_chat = Some(Chat::load_from_db(&context, id).await?);
|
||||
*chat_id = id;
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
println!("Selecting chat {}", chat_id);
|
||||
sel_chat = Some(Chat::load_from_db(context, chat_id)?);
|
||||
*context.cmdline_sel_chat_id.write().unwrap() = chat_id;
|
||||
}
|
||||
|
||||
ensure!(sel_chat.is_some(), "Failed to select chat");
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
|
||||
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None);
|
||||
let members = chat::get_chat_contacts(context, sel_chat.id);
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
let contact = Contact::get_by_id(&context, members[0]).await?;
|
||||
let contact = Contact::get_by_id(context, members[0])?;
|
||||
contact.get_addr().to_string()
|
||||
} else {
|
||||
format!("{} member(s)", members.len())
|
||||
@@ -591,7 +597,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(&context).await {
|
||||
match sel_chat.get_profile_image(context) {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
@@ -599,42 +605,38 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
_ => "".to_string(),
|
||||
},
|
||||
);
|
||||
log_msglist(&context, &msglist).await?;
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||
log_msg(&context, "Draft", &draft).await;
|
||||
log_msglist(context, &msglist)?;
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
|
||||
log_msg(context, "Draft", &draft);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} messages.",
|
||||
sel_chat.get_id().get_msg_cnt(&context).await
|
||||
);
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
|
||||
chat::marknoticed_chat(context, sel_chat.get_id())?;
|
||||
}
|
||||
"createchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id: libc::c_int = arg1.parse()?;
|
||||
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
|
||||
let chat_id = chat::create_by_contact_id(context, contact_id as u32)?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
}
|
||||
"createchatbymsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
let chat_id = chat::create_by_msg_id(&context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(&context, chat_id).await?;
|
||||
let chat_id = chat::create_by_msg_id(context, msg_id)?;
|
||||
let chat = Chat::load_from_db(context, chat_id)?;
|
||||
|
||||
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,);
|
||||
}
|
||||
"creategroup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
|
||||
let chat_id = chat::create_group_chat(context, VerifiedStatus::Unverified, arg1)?;
|
||||
|
||||
println!("Group#{} created successfully.", chat_id);
|
||||
}
|
||||
"createverified" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
|
||||
let chat_id = chat::create_group_chat(context, VerifiedStatus::Verified, arg1)?;
|
||||
|
||||
println!("VerifiedGroup#{} created successfully.", chat_id);
|
||||
}
|
||||
@@ -644,12 +646,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id_0: libc::c_int = arg1.parse()?;
|
||||
if chat::add_contact_to_chat(
|
||||
&context,
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_0 as u32,
|
||||
)
|
||||
.await
|
||||
{
|
||||
) {
|
||||
println!("Contact added to chat.");
|
||||
} else {
|
||||
bail!("Cannot add contact to chat.");
|
||||
@@ -660,18 +660,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id_1: libc::c_int = arg1.parse()?;
|
||||
chat::remove_contact_from_chat(
|
||||
&context,
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_1 as u32,
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
println!("Contact added to chat.");
|
||||
}
|
||||
"groupname" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
chat::set_chat_name(context, sel_chat.as_ref().unwrap().get_id(), arg1)?;
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
@@ -679,27 +678,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||
|
||||
chat::set_chat_profile_image(&context, sel_chat.as_ref().unwrap().get_id(), arg1)
|
||||
.await?;
|
||||
chat::set_chat_profile_image(context, sel_chat.as_ref().unwrap().get_id(), arg1)?;
|
||||
|
||||
println!("Chat image set");
|
||||
}
|
||||
"chatinfo" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
|
||||
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id());
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
log_contactlist(context, &contacts);
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id()
|
||||
)
|
||||
.await,
|
||||
),
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -707,13 +703,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id = arg1.parse().unwrap_or_default();
|
||||
let locations = location::get_range(
|
||||
&context,
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
);
|
||||
let default_marker = "-".to_string();
|
||||
for location in &locations {
|
||||
let marker = location.marker.as_ref().unwrap_or(&default_marker);
|
||||
@@ -739,12 +734,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||
|
||||
let seconds = arg1.parse()?;
|
||||
location::send_locations_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await;
|
||||
location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds);
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -759,7 +749,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
let continue_streaming = location::set(context, latitude, longitude, 0.);
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
@@ -767,7 +757,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
}
|
||||
"dellocations" => {
|
||||
location::delete_all(&context).await?;
|
||||
location::delete_all(context)?;
|
||||
}
|
||||
"send" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -775,11 +765,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let msg = format!("{} {}", arg1, arg2);
|
||||
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
||||
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), msg)?;
|
||||
}
|
||||
"sendempty" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), "".into())?;
|
||||
}
|
||||
"sendimage" | "sendfile" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -794,7 +784,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
if !arg2.is_empty() {
|
||||
msg.set_text(Some(arg2.to_string()));
|
||||
}
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?;
|
||||
}
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
@@ -805,9 +795,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
let msglist = context.search_msgs(chat, arg1).await;
|
||||
let msglist = context.search_msgs(chat, arg1);
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
log_msglist(context, &msglist)?;
|
||||
println!("{} messages.", msglist.len());
|
||||
}
|
||||
"draft" => {
|
||||
@@ -820,16 +810,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, Some(&mut draft))
|
||||
.await;
|
||||
.set_draft(context, Some(&mut draft));
|
||||
println!("Draft saved.");
|
||||
} else {
|
||||
sel_chat
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, None)
|
||||
.await;
|
||||
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
|
||||
println!("Draft deleted.");
|
||||
}
|
||||
}
|
||||
@@ -840,22 +824,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(arg1.to_string()));
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
chat::add_device_msg(context, None, Some(&mut msg))?;
|
||||
}
|
||||
"updatedevicechats" => {
|
||||
context.update_device_chats().await?;
|
||||
context.update_device_chats()?;
|
||||
}
|
||||
"listmedia" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let images = chat::get_chat_media(
|
||||
&context,
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
Viewtype::Image,
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
)
|
||||
.await;
|
||||
);
|
||||
println!("{} images or videos: ", images.len());
|
||||
for (i, data) in images.iter().enumerate() {
|
||||
if 0 == i {
|
||||
@@ -864,57 +847,36 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
print!(", {}", data);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
print!("\n");
|
||||
}
|
||||
"archive" | "unarchive" | "pin" | "unpin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id
|
||||
.set_visibility(
|
||||
&context,
|
||||
match arg0 {
|
||||
"archive" => ChatVisibility::Archived,
|
||||
"unarchive" | "unpin" => ChatVisibility::Normal,
|
||||
"pin" => ChatVisibility::Pinned,
|
||||
_ => panic!("Unexpected command (This should never happen)"),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
chat_id.set_visibility(
|
||||
context,
|
||||
match arg0 {
|
||||
"archive" => ChatVisibility::Archived,
|
||||
"unarchive" | "unpin" => ChatVisibility::Normal,
|
||||
"pin" => ChatVisibility::Pinned,
|
||||
_ => panic!("Unexpected command (This should never happen)"),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
"delchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat_id.delete(&context).await?;
|
||||
chat_id.delete(context)?;
|
||||
}
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await;
|
||||
let res = message::get_msg_info(context, id);
|
||||
println!("{}", res);
|
||||
}
|
||||
"openfile" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let msg = Message::load_from_db(&context, id).await?;
|
||||
let filepath = msg.get_file(&context);
|
||||
ensure!(filepath.is_some(), "Message has no file.");
|
||||
let filepath = filepath.unwrap();
|
||||
open::that(filepath)?;
|
||||
}
|
||||
"download" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let path = if !arg2.is_empty() {
|
||||
Some(arg2.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
message::schedule_download(&context, id, path).await?;
|
||||
}
|
||||
"listfresh" => {
|
||||
let msglist = context.get_fresh_msgs().await;
|
||||
let msglist = context.get_fresh_msgs();
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
log_msglist(context, &msglist)?;
|
||||
print!("{} fresh messages.", msglist.len());
|
||||
}
|
||||
"forward" => {
|
||||
@@ -926,38 +888,37 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let mut msg_ids = [MsgId::new(0); 1];
|
||||
let chat_id = ChatId::new(arg2.parse()?);
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
chat::forward_msgs(&context, &msg_ids, chat_id).await?;
|
||||
chat::forward_msgs(context, &msg_ids, chat_id)?;
|
||||
}
|
||||
"markseen" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0)];
|
||||
let mut msg_ids = [MsgId::new(0); 1];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::markseen_msgs(&context, msg_ids).await;
|
||||
message::markseen_msgs(context, &msg_ids);
|
||||
}
|
||||
"star" | "unstar" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0); 1];
|
||||
let mut msg_ids = [MsgId::new(0); 1];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::star_msgs(&context, msg_ids, arg0 == "star").await;
|
||||
message::star_msgs(context, &msg_ids, arg0 == "star");
|
||||
}
|
||||
"delmsg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut ids = [MsgId::new(0); 1];
|
||||
ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::delete_msgs(&context, &ids).await;
|
||||
message::delete_msgs(context, &ids);
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
context,
|
||||
if arg0 == "listverified" {
|
||||
0x1 | 0x2
|
||||
} else {
|
||||
0x2
|
||||
},
|
||||
Some(arg1),
|
||||
)
|
||||
.await?;
|
||||
log_contactlist(&context, &contacts).await;
|
||||
)?;
|
||||
log_contactlist(context, &contacts);
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
@@ -965,30 +926,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
if !arg2.is_empty() {
|
||||
let book = format!("{}\n{}", arg1, arg2);
|
||||
Contact::add_address_book(&context, book).await?;
|
||||
Contact::add_address_book(context, book)?;
|
||||
} else {
|
||||
Contact::create(&context, "", arg1).await?;
|
||||
Contact::create(context, "", arg1)?;
|
||||
}
|
||||
}
|
||||
"contactinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id)?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
match contact.get_profile_image(&context).await {
|
||||
match contact.get_profile_image(context) {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
res += &Contact::get_encrinfo(&context, contact_id).await?;
|
||||
res += &Contact::get_encrinfo(context, contact_id)?;
|
||||
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, Some(contact_id)).await?;
|
||||
let chatlist = Chatlist::try_load(context, 0, None, Some(contact_id))?;
|
||||
let chatlist_cnt = chatlist.len();
|
||||
if chatlist_cnt > 0 {
|
||||
res += &format!(
|
||||
@@ -999,7 +960,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
if 0 != i {
|
||||
res += ", ";
|
||||
}
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
|
||||
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
|
||||
}
|
||||
}
|
||||
@@ -1008,11 +969,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"delcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
Contact::delete(&context, arg1.parse()?).await?;
|
||||
Contact::delete(context, arg1.parse()?)?;
|
||||
}
|
||||
"checkqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let res = check_qr(&context, arg1).await;
|
||||
let res = check_qr(context, arg1);
|
||||
println!(
|
||||
"state={}, id={}, text1={:?}, text2={:?}",
|
||||
res.get_state(),
|
||||
@@ -1023,7 +984,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
match set_config_from_qr(context, arg1) {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Err(err) => println!("Cannot set config from QR code: {:?}", err),
|
||||
}
|
||||
@@ -1051,7 +1012,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
|
||||
// let event = arg1.parse()?;
|
||||
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// let r = context.call_cb(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
// event, event as usize, r as libc::c_int,
|
||||
@@ -1060,27 +1021,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = dc_read_file(&context, &arg1).await {
|
||||
if let Ok(buf) = dc_read_file(context, &arg1) {
|
||||
let (width, height) = dc_get_filemeta(&buf)?;
|
||||
println!("width={}, height={}", width, height);
|
||||
} else {
|
||||
bail!("Command failed.");
|
||||
}
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
let seconds = arg1.parse()?;
|
||||
let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?;
|
||||
let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?;
|
||||
println!(
|
||||
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
|
||||
seconds, device_cnt, server_cnt
|
||||
);
|
||||
}
|
||||
"emptyserver" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <flags> missing");
|
||||
|
||||
message::dc_empty_server(&context, arg1.parse()?).await;
|
||||
message::dc_empty_server(context, arg1.parse()?);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
|
||||
@@ -6,21 +6,27 @@
|
||||
|
||||
#[macro_use]
|
||||
extern crate deltachat;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::{bail, Error};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use deltachat::job::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::Event;
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::config::OutputStreamType;
|
||||
use rustyline::error::ReadlineError;
|
||||
@@ -33,83 +39,179 @@ use rustyline::{
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: Event) {
|
||||
let yellow = Color::Yellow.normal();
|
||||
// Event Handler
|
||||
|
||||
fn receive_event(_context: &Context, event: Event) {
|
||||
match event {
|
||||
Event::Info(msg) => {
|
||||
/* do not show the event as this would fill the screen */
|
||||
info!("{}", msg);
|
||||
println!("{}", msg);
|
||||
}
|
||||
Event::SmtpConnected(msg) => {
|
||||
info!("[SMTP_CONNECTED] {}", msg);
|
||||
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
|
||||
}
|
||||
Event::ImapConnected(msg) => {
|
||||
info!("[IMAP_CONNECTED] {}", msg);
|
||||
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
|
||||
}
|
||||
Event::SmtpMessageSent(msg) => {
|
||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
|
||||
}
|
||||
Event::Warning(msg) => {
|
||||
warn!("{}", msg);
|
||||
println!("[Warning] {}", msg);
|
||||
}
|
||||
Event::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
|
||||
}
|
||||
Event::ErrorNetwork(msg) => {
|
||||
error!("[NETWORK] msg={}", msg);
|
||||
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
|
||||
}
|
||||
Event::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
|
||||
}
|
||||
Event::MsgsChanged { chat_id, msg_id } => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!(
|
||||
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
|
||||
chat_id, msg_id,
|
||||
))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
|
||||
chat_id, msg_id,
|
||||
);
|
||||
}
|
||||
Event::ContactsChanged(_) => {
|
||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
|
||||
}
|
||||
Event::LocationChanged(contact) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
|
||||
contact,
|
||||
);
|
||||
}
|
||||
Event::ConfigureProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||
progress,
|
||||
);
|
||||
}
|
||||
Event::ImexProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||
progress,
|
||||
);
|
||||
}
|
||||
Event::ImexFileWritten(file) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
Event::ChatModified(chat) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
|
||||
print!(
|
||||
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
|
||||
chat
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
info!("Received {:?}", event);
|
||||
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Threads for waiting for messages and for jobs
|
||||
|
||||
lazy_static! {
|
||||
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
|
||||
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
|
||||
}
|
||||
|
||||
struct Handle {
|
||||
handle_imap: Option<std::thread::JoinHandle<()>>,
|
||||
handle_mvbox: Option<std::thread::JoinHandle<()>>,
|
||||
handle_sentbox: Option<std::thread::JoinHandle<()>>,
|
||||
handle_smtp: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
macro_rules! while_running {
|
||||
($code:block) => {
|
||||
if IS_RUNNING.load(Ordering::Relaxed) {
|
||||
$code
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn start_threads(c: Arc<RwLock<Context>>) {
|
||||
if HANDLE.clone().lock().unwrap().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Starting threads");
|
||||
IS_RUNNING.store(true, Ordering::Relaxed);
|
||||
|
||||
let ctx = c.clone();
|
||||
let handle_imap = std::thread::spawn(move || loop {
|
||||
while_running!({
|
||||
perform_inbox_jobs(&ctx.read().unwrap());
|
||||
perform_inbox_fetch(&ctx.read().unwrap());
|
||||
while_running!({
|
||||
let context = ctx.read().unwrap();
|
||||
perform_inbox_idle(&context);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let ctx = c.clone();
|
||||
let handle_mvbox = std::thread::spawn(move || loop {
|
||||
while_running!({
|
||||
perform_mvbox_fetch(&ctx.read().unwrap());
|
||||
while_running!({
|
||||
perform_mvbox_idle(&ctx.read().unwrap());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let ctx = c.clone();
|
||||
let handle_sentbox = std::thread::spawn(move || loop {
|
||||
while_running!({
|
||||
perform_sentbox_fetch(&ctx.read().unwrap());
|
||||
while_running!({
|
||||
perform_sentbox_idle(&ctx.read().unwrap());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let ctx = c;
|
||||
let handle_smtp = std::thread::spawn(move || loop {
|
||||
while_running!({
|
||||
perform_smtp_jobs(&ctx.read().unwrap());
|
||||
while_running!({
|
||||
perform_smtp_idle(&ctx.read().unwrap());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
*HANDLE.clone().lock().unwrap() = Some(Handle {
|
||||
handle_imap: Some(handle_imap),
|
||||
handle_mvbox: Some(handle_mvbox),
|
||||
handle_sentbox: Some(handle_sentbox),
|
||||
handle_smtp: Some(handle_smtp),
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_threads(context: &Context) {
|
||||
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
|
||||
println!("Stopping threads");
|
||||
IS_RUNNING.store(false, Ordering::Relaxed);
|
||||
|
||||
interrupt_inbox_idle(context);
|
||||
interrupt_mvbox_idle(context);
|
||||
interrupt_sentbox_idle(context);
|
||||
interrupt_smtp_idle(context);
|
||||
|
||||
handle.handle_imap.take().unwrap().join().unwrap();
|
||||
handle.handle_mvbox.take().unwrap().join().unwrap();
|
||||
handle.handle_sentbox.take().unwrap().join().unwrap();
|
||||
handle.handle_smtp.take().unwrap().join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// === The main loop
|
||||
|
||||
struct DcHelper {
|
||||
@@ -146,8 +248,10 @@ const IMEX_COMMANDS: [&str; 12] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 9] = [
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
"info",
|
||||
"open",
|
||||
"close",
|
||||
"set",
|
||||
"get",
|
||||
"oauth2",
|
||||
@@ -186,11 +290,9 @@ const CHAT_COMMANDS: [&str; 26] = [
|
||||
"unpin",
|
||||
"delchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"openfile",
|
||||
"download",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"markseen",
|
||||
@@ -206,17 +308,8 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
"getqr",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
"quit",
|
||||
"help",
|
||||
"estimatedeletion",
|
||||
const MISC_COMMANDS: [&str; 9] = [
|
||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
@@ -268,81 +361,69 @@ impl Highlighter for DcHelper {
|
||||
|
||||
impl Helper for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
return Err(format_err!("No db-name specified"));
|
||||
}
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
receive_event(event);
|
||||
}
|
||||
});
|
||||
let context = Context::new(
|
||||
Box::new(receive_event),
|
||||
"CLI".into(),
|
||||
Path::new(&args[1]).to_path_buf(),
|
||||
)?;
|
||||
|
||||
println!("Delta Chat Core is awaiting your commands.");
|
||||
|
||||
let ctx = Arc::new(RwLock::new(context));
|
||||
|
||||
let config = Config::builder()
|
||||
.history_ignore_space(true)
|
||||
.completion_type(CompletionType::List)
|
||||
.edit_mode(EditMode::Emacs)
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
let mut selected_chat = ChatId::default();
|
||||
let (reader_s, reader_r) = async_std::sync::channel(100);
|
||||
let input_loop = async_std::task::spawn_blocking(move || {
|
||||
let h = DcHelper {
|
||||
completer: FilenameCompleter::new(),
|
||||
highlighter: MatchingBracketHighlighter::new(),
|
||||
hinter: HistoryHinter {},
|
||||
};
|
||||
let mut rl = Editor::with_config(config);
|
||||
rl.set_helper(Some(h));
|
||||
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
||||
if rl.load_history(".dc-history.txt").is_err() {
|
||||
println!("No previous history.");
|
||||
}
|
||||
let h = DcHelper {
|
||||
completer: FilenameCompleter::new(),
|
||||
highlighter: MatchingBracketHighlighter::new(),
|
||||
hinter: HistoryHinter {},
|
||||
};
|
||||
let mut rl = Editor::with_config(config);
|
||||
rl.set_helper(Some(h));
|
||||
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
||||
if rl.load_history(".dc-history.txt").is_err() {
|
||||
println!("No previous history.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let p = "> ";
|
||||
let readline = rl.readline(&p);
|
||||
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str());
|
||||
async_std::task::block_on(reader_s.send(line));
|
||||
}
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
drop(reader_s);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err);
|
||||
drop(reader_s);
|
||||
break;
|
||||
loop {
|
||||
let p = "> ";
|
||||
let readline = rl.readline(&p);
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str());
|
||||
let ctx = ctx.clone();
|
||||
match handle_cmd(line.trim(), ctx) {
|
||||
Ok(ExitResult::Continue) => {}
|
||||
Ok(ExitResult::Exit) => break,
|
||||
Err(err) => println!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rl.save_history(".dc-history.txt")?;
|
||||
println!("history saved");
|
||||
Ok::<_, Error>(())
|
||||
});
|
||||
|
||||
while let Ok(line) = reader_r.recv().await {
|
||||
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
|
||||
Ok(ExitResult::Continue) => {}
|
||||
Ok(ExitResult::Exit) => break,
|
||||
Err(err) => println!("Error: {}", err),
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
context.stop_io().await;
|
||||
input_loop.await?;
|
||||
rl.save_history(".dc-history.txt")?;
|
||||
println!("history saved");
|
||||
{
|
||||
stop_threads(&ctx.read().unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -353,29 +434,43 @@ enum ExitResult {
|
||||
Exit,
|
||||
}
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
let arg0 = args.next().unwrap_or_default();
|
||||
let arg1 = args.next().unwrap_or_default();
|
||||
|
||||
match arg0 {
|
||||
"connect" => {
|
||||
ctx.start_io().await;
|
||||
start_threads(ctx);
|
||||
}
|
||||
"disconnect" => {
|
||||
ctx.stop_io().await;
|
||||
stop_threads(&ctx.read().unwrap());
|
||||
}
|
||||
"smtp-jobs" => {
|
||||
if HANDLE.clone().lock().unwrap().is_some() {
|
||||
println!("smtp-jobs are already running in a thread.",);
|
||||
} else {
|
||||
perform_smtp_jobs(&ctx.read().unwrap());
|
||||
}
|
||||
}
|
||||
"imap-jobs" => {
|
||||
if HANDLE.clone().lock().unwrap().is_some() {
|
||||
println!("inbox-jobs are already running in a thread.");
|
||||
} else {
|
||||
perform_inbox_jobs(&ctx.read().unwrap());
|
||||
}
|
||||
}
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
start_threads(ctx.clone());
|
||||
ctx.read().unwrap().configure();
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
|
||||
let oauth2_url =
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
|
||||
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
||||
let oauth2_url = dc_get_oauth2_url(
|
||||
&ctx.read().unwrap(),
|
||||
&addr,
|
||||
"chat.delta:/com.b44t.messenger",
|
||||
);
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
} else {
|
||||
@@ -390,10 +485,11 @@ async fn handle_cmd(
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
if let Some(mut qr) =
|
||||
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
|
||||
{
|
||||
start_threads(ctx.clone());
|
||||
if let Some(mut qr) = dc_get_securejoin_qr(
|
||||
&ctx.read().unwrap(),
|
||||
ChatId::new(arg1.parse().unwrap_or_default()),
|
||||
) {
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
@@ -409,23 +505,23 @@ async fn handle_cmd(
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
start_threads(ctx.clone());
|
||||
if !arg0.is_empty() {
|
||||
dc_join_securejoin(&ctx, arg1).await;
|
||||
dc_join_securejoin(&ctx.read().unwrap(), arg1);
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
||||
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
|
||||
}
|
||||
|
||||
Ok(ExitResult::Continue)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
pub fn main() -> Result<(), failure::Error> {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
||||
let args = std::env::args().collect();
|
||||
async_std::task::block_on(async move { start(args).await })?;
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
main_0(args)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
extern crate deltachat;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{thread, time};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use deltachat::chat;
|
||||
@@ -5,96 +9,103 @@ use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::job::{
|
||||
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
|
||||
perform_smtp_jobs,
|
||||
};
|
||||
use deltachat::Event;
|
||||
|
||||
fn cb(event: Event) {
|
||||
fn cb(_ctx: &Context, event: Event) {
|
||||
print!("[{:?}]", event);
|
||||
|
||||
match event {
|
||||
Event::ConfigureProgress(progress) => {
|
||||
log::info!("progress: {}", progress);
|
||||
println!(" progress: {}", progress);
|
||||
}
|
||||
Event::Info(msg) => {
|
||||
log::info!("{}", msg);
|
||||
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||
println!(" {}", msg);
|
||||
}
|
||||
Event::Warning(msg) => {
|
||||
log::warn!("{}", msg);
|
||||
}
|
||||
Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||
log::error!("{}", msg);
|
||||
}
|
||||
event => {
|
||||
log::info!("{:?}", event);
|
||||
_ => {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into())
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
log::info!("info: {:#?}", info);
|
||||
println!("creating database {:?}", dbfile);
|
||||
let ctx =
|
||||
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
|
||||
let running = Arc::new(RwLock::new(true));
|
||||
let info = ctx.get_info();
|
||||
let duration = time::Duration::from_millis(4000);
|
||||
println!("info: {:#?}", info);
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
let events_spawn = async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
cb(event);
|
||||
let ctx = Arc::new(ctx);
|
||||
let ctx1 = ctx.clone();
|
||||
let r1 = running.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
while *r1.read().unwrap() {
|
||||
perform_inbox_jobs(&ctx1);
|
||||
if *r1.read().unwrap() {
|
||||
perform_inbox_fetch(&ctx1);
|
||||
|
||||
if *r1.read().unwrap() {
|
||||
perform_inbox_idle(&ctx1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::info!("configuring");
|
||||
let ctx1 = ctx.clone();
|
||||
let r1 = running.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
while *r1.read().unwrap() {
|
||||
perform_smtp_jobs(&ctx1);
|
||||
if *r1.read().unwrap() {
|
||||
perform_smtp_idle(&ctx1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
println!("configuring");
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
assert_eq!(args.len(), 3, "requires email password");
|
||||
let email = args[1].clone();
|
||||
let pw = args[2].clone();
|
||||
ctx.set_config(config::Config::Addr, Some(&email))
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(config::Config::MailPw, Some(&pw))
|
||||
.await
|
||||
assert_eq!(args.len(), 2, "missing password");
|
||||
let pw = args[1].clone();
|
||||
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
||||
.unwrap();
|
||||
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
||||
ctx.configure();
|
||||
|
||||
ctx.configure().await.unwrap();
|
||||
thread::sleep(duration);
|
||||
|
||||
log::info!("------ RUN ------");
|
||||
ctx.start_io().await;
|
||||
log::info!("--- SENDING A MESSAGE ---");
|
||||
println!("sending a message");
|
||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
|
||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
|
||||
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
|
||||
|
||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
|
||||
|
||||
for i in 0..1 {
|
||||
log::info!("sending message {}", i);
|
||||
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// wait for the message to be sent out
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
log::info!("fetching chats..");
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||
println!("fetching chats..");
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
|
||||
|
||||
for i in 0..chats.len() {
|
||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("[{}] msg: {:?}", i, msg);
|
||||
let summary = chats.get_summary(&ctx, 0, None);
|
||||
let text1 = summary.get_text1();
|
||||
let text2 = summary.get_text2();
|
||||
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
|
||||
}
|
||||
|
||||
log::info!("stopping");
|
||||
ctx.stop_io().await;
|
||||
log::info!("closing");
|
||||
drop(ctx);
|
||||
events_spawn.await;
|
||||
thread::sleep(duration);
|
||||
|
||||
println!("stopping threads");
|
||||
|
||||
*running.write().unwrap() = false;
|
||||
deltachat::job::interrupt_inbox_idle(&ctx);
|
||||
deltachat::job::interrupt_smtp_idle(&ctx);
|
||||
|
||||
println!("joining");
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
println!("closing");
|
||||
}
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
0.900.0 (DRAFT)
|
||||
---------------
|
||||
|
||||
- refactored internals to use plugin-approach
|
||||
|
||||
- introduced PerAccount and Global hooks that plugins can implement
|
||||
|
||||
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
|
||||
|
||||
- introduced two documented examples for an echo and a group-membership
|
||||
tracking plugin.
|
||||
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -113,10 +113,10 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
|
||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||
|
||||
|
||||
Building manylinux based wheels
|
||||
====================================
|
||||
Building manylinux1 based wheels
|
||||
================================
|
||||
|
||||
Building portable manylinux wheels which come with libdeltachat.so
|
||||
Building portable manylinux1 wheels which come with libdeltachat.so
|
||||
can be done with docker-tooling.
|
||||
|
||||
using docker pull / premade images
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
high level API reference
|
||||
========================
|
||||
|
||||
.. note::
|
||||
|
||||
This API is work in progress and may change in versions prior to 1.0.
|
||||
|
||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||
other classes)
|
||||
- :class:`deltachat.contact.Contact`
|
||||
|
||||
7
python/doc/capi.rst
Normal file
7
python/doc/capi.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
C deltachat interface
|
||||
=====================
|
||||
|
||||
See :doc:`lapi` for accessing many of the below functions
|
||||
through the ``deltachat.capi.lib`` namespace.
|
||||
|
||||
@@ -1,60 +1,37 @@
|
||||
|
||||
|
||||
examples
|
||||
========
|
||||
|
||||
|
||||
Playing around on the commandline
|
||||
----------------------------------
|
||||
|
||||
Once you have :doc:`installed deltachat bindings <install>`
|
||||
you need email/password credentials for an IMAP/SMTP account.
|
||||
Delta Chat developers and the CI system use a special URL to create
|
||||
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
||||
you can start playing from the python interpreter commandline.
|
||||
For example you can type ``python`` and then::
|
||||
|
||||
Receiving a Chat message from the command line
|
||||
----------------------------------------------
|
||||
# instantiate and configure deltachat account
|
||||
import deltachat
|
||||
ac = deltachat.Account("/tmp/db")
|
||||
|
||||
Here is a simple bot that:
|
||||
# start configuration activity and smtp/imap threads
|
||||
ac.start_threads()
|
||||
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
||||
|
||||
- receives a message and sends back ("echoes") a message
|
||||
# create a contact and send a message
|
||||
contact = ac.create_contact("someother@email.address")
|
||||
chat = ac.create_chat_by_contact(contact)
|
||||
chat.send_text("hi from the python interpreter command line")
|
||||
|
||||
- terminates the bot if the message `/quit` is sent
|
||||
Checkout our :doc:`api` for the various high-level things you can do
|
||||
to send/receive messages, create contacts and chats.
|
||||
|
||||
.. include:: ../examples/echo_and_quit.py
|
||||
:literal:
|
||||
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
$ cd examples
|
||||
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
|
||||
|
||||
While this process is running you can start sending chat messages
|
||||
to `ADDRESS`.
|
||||
|
||||
Track member additions and removals in a group
|
||||
----------------------------------------------
|
||||
|
||||
Here is a simple bot that:
|
||||
|
||||
- echoes messages sent to it
|
||||
|
||||
- tracks if configuration completed
|
||||
|
||||
- tracks member additions and removals for all chat groups
|
||||
|
||||
.. include:: ../examples/group_tracking.py
|
||||
:literal:
|
||||
|
||||
With this file in your working directory you can run the bot
|
||||
by specifying a database path, an e-mail address and password of
|
||||
a SMTP-IMAP account::
|
||||
|
||||
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
||||
|
||||
When this process is running you can start sending chat messages
|
||||
to `ADDRESS`.
|
||||
|
||||
Writing bots for real
|
||||
Looking at a real example
|
||||
-------------------------
|
||||
|
||||
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||
contains a little framework for writing deltachat bots in Python.
|
||||
contains a real-life example of Python bindings usage.
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ deltachat python bindings
|
||||
The ``deltachat`` Python package provides two layers of bindings for the
|
||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||
|
||||
- :doc:`api` is a high level interface to deltachat-core.
|
||||
|
||||
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||
|
||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||
@@ -29,7 +28,6 @@ getting started
|
||||
changelog
|
||||
api
|
||||
lapi
|
||||
plugins
|
||||
|
||||
..
|
||||
Indices and tables
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
Implementing Plugin Hooks
|
||||
==========================
|
||||
|
||||
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
|
||||
for managing global and per-account plugin registration, and performing
|
||||
hook calls. There are two kinds of plugins:
|
||||
|
||||
- Global plugins that are active for all accounts; they can implement
|
||||
hooks at account-creation and account-shutdown time.
|
||||
|
||||
- Account plugins that are only active during the lifetime of a
|
||||
single Account instance.
|
||||
|
||||
|
||||
Registering a plugin
|
||||
--------------------
|
||||
|
||||
.. autofunction:: deltachat.register_global_plugin
|
||||
:noindex:
|
||||
|
||||
.. automethod:: deltachat.account.Account.add_account_plugin
|
||||
:noindex:
|
||||
|
||||
|
||||
Per-Account Hook specifications
|
||||
-------------------------------
|
||||
|
||||
.. autoclass:: deltachat.hookspec.PerAccount
|
||||
:members:
|
||||
|
||||
|
||||
Global Hook specifications
|
||||
--------------------------
|
||||
|
||||
.. autoclass:: deltachat.hookspec.Global
|
||||
:members:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
# content of echo_and_quit.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class EchoPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.create_chat()
|
||||
addr = message.get_sender_contact().addr
|
||||
if message.is_system_message():
|
||||
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
|
||||
else:
|
||||
text = message.text
|
||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_message_delivered(self, message):
|
||||
print("ac_message_delivered", message)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
# content of group_tracking.py
|
||||
|
||||
from deltachat import account_hookimpl, run_cmdline
|
||||
|
||||
|
||||
class GroupTrackingPlugin:
|
||||
@account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
message.account.shutdown()
|
||||
else:
|
||||
# unconditionally accept the chat
|
||||
message.create_chat()
|
||||
addr = message.get_sender_contact().addr
|
||||
text = message.text
|
||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_outgoing_message(self, message):
|
||||
print("ac_outgoing_message:", message)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
print("ac_configure_completed:", success)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_chat_modified(self, chat):
|
||||
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, 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, message):
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
import pytest
|
||||
import py
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
from deltachat.events import FFIEventLogger
|
||||
|
||||
|
||||
@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')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating one echo_and_quit bot")
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
bot_chat = bot_contact.create_chat()
|
||||
bot_chat.send_text("hello")
|
||||
|
||||
lp.sec("waiting for the reply message from the bot to arrive")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply.chat == bot_chat
|
||||
assert "hello" in reply.text
|
||||
lp.sec("send quit sequence")
|
||||
bot_chat.send_text("/quit")
|
||||
botproc.wait()
|
||||
|
||||
|
||||
def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
||||
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
||||
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_configure_completed*
|
||||
""")
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
|
||||
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"))
|
||||
ch.add_contact(contact3)
|
||||
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert "hello" in reply.text
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_added {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*
|
||||
""".format(contact3.addr))
|
||||
@@ -1,7 +0,0 @@
|
||||
from __future__ import print_function
|
||||
from deltachat import capi
|
||||
from deltachat.capi import ffi, lib
|
||||
|
||||
if __name__ == "__main__":
|
||||
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
||||
lib.dc_stop_io(ctx)
|
||||
@@ -19,7 +19,6 @@ if __name__ == "__main__":
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
if target == 'release':
|
||||
cmd.append("--release")
|
||||
print("running:", " ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||
|
||||
|
||||
@@ -18,15 +18,10 @@ def main():
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
|
||||
install_requires=['cffi>=1.0.0', 'pluggy'],
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
entry_points = {
|
||||
'pytest11': [
|
||||
'deltachat.testplugin = deltachat.testplugin',
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import sys
|
||||
|
||||
from . import capi, const, hookspec # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .account import Account # 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 deltachat import capi, const
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.account import Account # noqa
|
||||
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
try:
|
||||
@@ -17,72 +10,67 @@ except DistributionNotFound:
|
||||
__version__ = "0.0.0.dev0-unknown"
|
||||
|
||||
|
||||
_DC_CALLBACK_MAP = {}
|
||||
|
||||
|
||||
@capi.ffi.def_extern()
|
||||
def py_dc_callback(ctx, evt, data1, data2):
|
||||
"""The global event handler.
|
||||
|
||||
CFFI only allows us to set one global event handler, so this one
|
||||
looks up the correct event handler for the given context.
|
||||
"""
|
||||
try:
|
||||
callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0)
|
||||
except AttributeError:
|
||||
# we are in a deep in GC-free/interpreter shutdown land
|
||||
# nothing much better to do here than:
|
||||
return 0
|
||||
|
||||
# the following code relates to the deltachat/_build.py's helper
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = get_dc_event_name(evt)
|
||||
event_sig_types = capi.lib.dc_get_event_signature_types(evt)
|
||||
if data1 and event_sig_types & 1:
|
||||
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
|
||||
if data2 and event_sig_types & 2:
|
||||
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
||||
try:
|
||||
if isinstance(data2, bytes):
|
||||
data2 = data2.decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
# XXX ignoring the decode error is not quite correct but for now
|
||||
# i don't want to hunt down encoding problems in the c lib
|
||||
pass
|
||||
try:
|
||||
ret = callback(ctx, evt_name, data1, data2)
|
||||
if ret is None:
|
||||
ret = 0
|
||||
assert isinstance(ret, int), repr(ret)
|
||||
if event_sig_types & 4:
|
||||
return ffi.cast('uintptr_t', ret)
|
||||
elif event_sig_types & 8:
|
||||
return ffi.cast('int', ret)
|
||||
except: # noqa
|
||||
raise
|
||||
ret = 0
|
||||
return ret
|
||||
|
||||
|
||||
def set_context_callback(dc_context, func):
|
||||
_DC_CALLBACK_MAP[dc_context] = func
|
||||
|
||||
|
||||
def clear_context_callback(dc_context):
|
||||
try:
|
||||
_DC_CALLBACK_MAP.pop(dc_context, None)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name, val in vars(const).items():
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
def register_global_plugin(plugin):
|
||||
""" Register a global plugin which implements one or more
|
||||
of the :class:`deltachat.hookspec.Global` hooks.
|
||||
"""
|
||||
gm = hookspec.Global._get_plugin_manager()
|
||||
gm.register(plugin)
|
||||
gm.check_pending()
|
||||
|
||||
|
||||
def unregister_global_plugin(plugin):
|
||||
gm = hookspec.Global._get_plugin_manager()
|
||||
gm.unregister(plugin)
|
||||
|
||||
|
||||
register_global_plugin(events)
|
||||
|
||||
|
||||
def run_cmdline(argv=None, account_plugins=None):
|
||||
""" Run a simple default command line app, registering the specified
|
||||
account plugins. """
|
||||
import argparse
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
|
||||
parser.add_argument("db", action="store", help="database file")
|
||||
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
|
||||
parser.add_argument("--email", action="store", help="email address")
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
ac = Account(args.db)
|
||||
|
||||
if args.show_ffi:
|
||||
ac.set_config("displayname", "bot")
|
||||
log = events.FFIEventLogger(ac)
|
||||
ac.add_account_plugin(log)
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
print("adding plugin", plugin)
|
||||
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"
|
||||
)
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
configtracker = ac.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
ac.start_io()
|
||||
|
||||
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||
|
||||
ac.wait_shutdown()
|
||||
|
||||
@@ -1,64 +1,79 @@
|
||||
import distutils.ccompiler
|
||||
import distutils.log
|
||||
import distutils.sysconfig
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import types
|
||||
from os.path import abspath
|
||||
from os.path import dirname as dn
|
||||
|
||||
import platform
|
||||
import os
|
||||
import cffi
|
||||
import shutil
|
||||
from os.path import dirname as dn
|
||||
from os.path import abspath
|
||||
|
||||
|
||||
def local_build_flags(projdir, target):
|
||||
"""Construct build flags for building against a checkout.
|
||||
|
||||
:param projdir: The root directory of the deltachat-core-rust project.
|
||||
:param target: The rust build target, `debug` or `release`.
|
||||
"""
|
||||
flags = types.SimpleNamespace()
|
||||
if platform.system() == 'Darwin':
|
||||
flags.libs = ['resolv', 'dl']
|
||||
flags.extra_link_args = [
|
||||
'-framework', 'CoreFoundation',
|
||||
'-framework', 'CoreServices',
|
||||
'-framework', 'Security',
|
||||
]
|
||||
elif platform.system() == 'Linux':
|
||||
flags.libs = ['rt', 'dl', 'm']
|
||||
flags.extra_link_args = []
|
||||
def ffibuilder():
|
||||
projdir = os.environ.get('DCC_RS_DEV')
|
||||
if not projdir:
|
||||
p = dn(dn(dn(dn(abspath(__file__)))))
|
||||
projdir = os.environ["DCC_RS_DEV"] = p
|
||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||
if projdir:
|
||||
if platform.system() == 'Darwin':
|
||||
libs = ['resolv', 'dl']
|
||||
extra_link_args = [
|
||||
'-framework', 'CoreFoundation',
|
||||
'-framework', 'CoreServices',
|
||||
'-framework', 'Security',
|
||||
]
|
||||
elif platform.system() == 'Linux':
|
||||
libs = ['rt', 'dl', 'm']
|
||||
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')
|
||||
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(objs[0]), objs
|
||||
incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
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.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags.objs[0]), flags.objs
|
||||
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
return flags
|
||||
|
||||
|
||||
def system_build_flags():
|
||||
"""Construct build flags for building against an installed libdeltachat."""
|
||||
flags = types.SimpleNamespace()
|
||||
flags.libs = ['deltachat']
|
||||
flags.objs = []
|
||||
flags.incs = []
|
||||
flags.extra_link_args = []
|
||||
|
||||
|
||||
def extract_functions(flags):
|
||||
"""Extract the function definitions from deltachat.h.
|
||||
|
||||
This creates a .h file with a single `#include <deltachat.h>` line
|
||||
in it. It then runs the C preprocessor to create an output file
|
||||
which contains all function definitions found in `deltachat.h`.
|
||||
"""
|
||||
libs = ['deltachat']
|
||||
objs = []
|
||||
incs = []
|
||||
extra_link_args = []
|
||||
builder = cffi.FFI()
|
||||
builder.set_source(
|
||||
'deltachat.capi',
|
||||
"""
|
||||
#include <deltachat.h>
|
||||
const char * dupstring_helper(const char* string)
|
||||
{
|
||||
return strdup(string);
|
||||
}
|
||||
int dc_get_event_signature_types(int e)
|
||||
{
|
||||
int result = 0;
|
||||
if (DC_EVENT_DATA1_IS_STRING(e))
|
||||
result |= 1;
|
||||
if (DC_EVENT_DATA2_IS_STRING(e))
|
||||
result |= 2;
|
||||
if (DC_EVENT_RETURNS_STRING(e))
|
||||
result |= 4;
|
||||
if (DC_EVENT_RETURNS_INT(e))
|
||||
result |= 8;
|
||||
return result;
|
||||
}
|
||||
""",
|
||||
include_dirs=incs,
|
||||
libraries=libs,
|
||||
extra_objects=objs,
|
||||
extra_link_args=extra_link_args,
|
||||
)
|
||||
builder.cdef("""
|
||||
typedef int... time_t;
|
||||
void free(void *ptr);
|
||||
extern const char * dupstring_helper(const char* string);
|
||||
extern int dc_get_event_signature_types(int);
|
||||
""")
|
||||
distutils.log.set_verbosity(distutils.log.INFO)
|
||||
cc = distutils.ccompiler.new_compiler(force=True)
|
||||
distutils.sysconfig.customize_compiler(cc)
|
||||
@@ -70,133 +85,20 @@ def extract_functions(flags):
|
||||
src_fp.write('#include <deltachat.h>')
|
||||
cc.preprocess(source=src_name,
|
||||
output_file=dst_name,
|
||||
include_dirs=flags.incs,
|
||||
include_dirs=incs,
|
||||
macros=[('PY_CFFI', '1')])
|
||||
with open(dst_name, "r") as dst_fp:
|
||||
return dst_fp.read()
|
||||
builder.cdef(dst_fp.read())
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def find_header(flags):
|
||||
"""Use the compiler to find the deltachat.h header location.
|
||||
|
||||
This uses a small utility in deltachat.h to find the location of
|
||||
the header file location.
|
||||
"""
|
||||
distutils.log.set_verbosity(distutils.log.INFO)
|
||||
cc = distutils.ccompiler.new_compiler(force=True)
|
||||
distutils.sysconfig.customize_compiler(cc)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
src_name = os.path.join(tmpdir, "where.c")
|
||||
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("""
|
||||
#include <stdio.h>
|
||||
#include <deltachat.h>
|
||||
|
||||
int main(void) {
|
||||
printf("%s", _dc_header_file_location());
|
||||
return 0;
|
||||
}
|
||||
"""))
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
cc.compile(sources=["where.c"],
|
||||
include_dirs=flags.incs,
|
||||
macros=[("PY_CFFI_INC", "1")])
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
cc.link_executable(objects=[obj_name],
|
||||
output_progname="where",
|
||||
output_dir=tmpdir)
|
||||
return subprocess.check_output(dst_name)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def extract_defines(flags):
|
||||
"""Extract the required #DEFINEs from deltachat.h.
|
||||
|
||||
Since #DEFINEs are interpreted by the C preprocessor we can not
|
||||
use the compiler to extract these and need to parse the header
|
||||
file ourselves.
|
||||
|
||||
The defines are returned in a string that can be passed to CFFIs
|
||||
cdef() method.
|
||||
"""
|
||||
header = find_header(flags)
|
||||
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
|
||||
# to build the list of prefixes to extract without
|
||||
# creation another capture group.
|
||||
DC_EVENT
|
||||
| DC_QR
|
||||
| DC_MSG
|
||||
| DC_LP
|
||||
| DC_EMPTY
|
||||
| DC_CERTCK
|
||||
| DC_STATE
|
||||
| DC_STR
|
||||
| DC_CONTACT_ID
|
||||
| DC_GCL
|
||||
| DC_CHAT
|
||||
| DC_PROVIDER
|
||||
| DC_KEY_GEN
|
||||
) # End of prefix matching
|
||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
||||
) # 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)
|
||||
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)
|
||||
|
||||
|
||||
def ffibuilder():
|
||||
projdir = os.environ.get('DCC_RS_DEV')
|
||||
if not projdir:
|
||||
p = dn(dn(dn(dn(abspath(__file__)))))
|
||||
projdir = os.environ["DCC_RS_DEV"] = p
|
||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||
if projdir:
|
||||
flags = local_build_flags(projdir, target)
|
||||
else:
|
||||
flags = system_build_flags()
|
||||
builder = cffi.FFI()
|
||||
builder.set_source(
|
||||
'deltachat.capi',
|
||||
"""
|
||||
#include <deltachat.h>
|
||||
int dc_event_has_string_data(int e)
|
||||
{
|
||||
return DC_EVENT_DATA2_IS_STRING(e);
|
||||
}
|
||||
""",
|
||||
include_dirs=flags.incs,
|
||||
libraries=flags.libs,
|
||||
extra_objects=flags.objs,
|
||||
extra_link_args=flags.extra_link_args,
|
||||
)
|
||||
builder.cdef("""
|
||||
typedef int... time_t;
|
||||
void free(void *ptr);
|
||||
extern int dc_event_has_string_data(int);
|
||||
extern "Python" uintptr_t py_dc_callback(
|
||||
dc_context_t* context,
|
||||
int event,
|
||||
uintptr_t data1,
|
||||
uintptr_t data2);
|
||||
""")
|
||||
function_defs = extract_functions(flags)
|
||||
defines = extract_defines(flags)
|
||||
builder.cdef(function_defs)
|
||||
builder.cdef(defines)
|
||||
return builder
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
""" Account class implementation. """
|
||||
|
||||
from __future__ import print_function
|
||||
from contextlib import contextmanager
|
||||
from email.utils import parseaddr
|
||||
from threading import Event
|
||||
import atexit
|
||||
import threading
|
||||
import os
|
||||
import time
|
||||
from array import array
|
||||
from queue import Queue
|
||||
|
||||
import deltachat
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
""" Account is missing `addr` and `mail_pw` config values. """
|
||||
from .eventlogger import EventLogger
|
||||
from .hookspec import get_plugin_manager, hookimpl
|
||||
|
||||
|
||||
class Account(object):
|
||||
@@ -26,53 +24,45 @@ class Account(object):
|
||||
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):
|
||||
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
will be created if it doesn't exist.
|
||||
:param logid: an optional logging prefix that should be used with
|
||||
the default internal logging.
|
||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||
:param debug: turn on debug logging for events.
|
||||
"""
|
||||
# initialize per-account plugin system
|
||||
self._pm = hookspec.PerAccount._make_plugin_manager()
|
||||
self._logging = logging
|
||||
self._dc_context = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
||||
_destroy_dc_context,
|
||||
)
|
||||
self._evlogger = EventLogger(self, logid, debug)
|
||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||
|
||||
self.add_account_plugin(self)
|
||||
# register event call back and initialize plugin system
|
||||
def _ll_event(ctx, evt_name, data1, data2):
|
||||
assert ctx == self._dc_context
|
||||
self.pluggy.hook.process_low_level_event(
|
||||
account=self, event_name=evt_name, data1=data1, data2=data2
|
||||
)
|
||||
|
||||
self.db_path = db_path
|
||||
self.pluggy = get_plugin_manager()
|
||||
self.pluggy.register(self._evlogger)
|
||||
deltachat.set_context_callback(self._dc_context, _ll_event)
|
||||
|
||||
# open database
|
||||
if hasattr(db_path, "encode"):
|
||||
db_path = db_path.encode("utf8")
|
||||
|
||||
self._dc_context = ffi.gc(
|
||||
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))
|
||||
|
||||
self._shutdown_event = Event()
|
||||
self._event_thread = EventThread(self)
|
||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||
self._configkeys = self.get_config("sys.config_keys").split()
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
# def __del__(self):
|
||||
# self.shutdown()
|
||||
|
||||
def log(self, msg):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
@@ -144,15 +134,16 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs):
|
||||
""" update config values.
|
||||
def configure(self, **kwargs):
|
||||
""" set config values and configure this account.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
values need to be unicode.
|
||||
:returns: None
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
for name, value in kwargs.items():
|
||||
self.set_config(name, value)
|
||||
lib.dc_configure(self._dc_context)
|
||||
|
||||
def is_configured(self):
|
||||
""" determine if the account is configured already; an initial connection
|
||||
@@ -160,7 +151,7 @@ class Account(object):
|
||||
|
||||
:returns: True if account is configured.
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
return lib.dc_is_configured(self._dc_context)
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
"""Set self avatar.
|
||||
@@ -211,43 +202,23 @@ class Account(object):
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
self.check_is_configured()
|
||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
def create_contact(self, email, name=None):
|
||||
""" create a (new) Contact. If there already is a Contact
|
||||
with that e-mail address, it is unblocked and its name is
|
||||
updated.
|
||||
|
||||
Calling this method will always resulut in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
with that e-mail address, it is unblocked and its display
|
||||
`name` is updated if specified.
|
||||
|
||||
:param obj: email-address, Account or Contact instance.
|
||||
:param name: (optional) display name for this contact
|
||||
:param email: email-address (text type)
|
||||
:param name: display name for this contact (optional)
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
addr, displayname = obj.get_config("addr"), obj.get_config("displayname")
|
||||
elif isinstance(obj, Contact):
|
||||
if obj.account != self:
|
||||
raise ValueError("account mismatch {}".format(obj))
|
||||
addr, displayname = obj.addr, obj.name
|
||||
elif isinstance(obj, str):
|
||||
displayname, addr = parseaddr(obj)
|
||||
else:
|
||||
raise TypeError("don't know how to create chat for %r" % (obj, ))
|
||||
|
||||
if name is None and displayname:
|
||||
name = displayname
|
||||
return self._create_contact(addr, name)
|
||||
|
||||
def _create_contact(self, addr, name):
|
||||
addr = as_dc_charpointer(addr)
|
||||
name = as_dc_charpointer(name)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
|
||||
return Contact(self, contact_id)
|
||||
email = as_dc_charpointer(email)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return Contact(self._dc_context, contact_id)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
""" delete a Contact.
|
||||
@@ -256,25 +227,10 @@ class Account(object):
|
||||
:returns: True if deletion succeeded (contact was deleted)
|
||||
"""
|
||||
contact_id = contact.id
|
||||
assert contact.account == self
|
||||
assert contact._dc_context == self._dc_context
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email):
|
||||
""" 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)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
|
||||
@@ -294,39 +250,52 @@ class Account(object):
|
||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
""" 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_by_contact(self, contact):
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
def create_chat(self, obj):
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
:param contact: chat_id (int) or contact object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
if hasattr(contact, "id"):
|
||||
if contact._dc_context != self._dc_context:
|
||||
raise ValueError("Contact belongs to a different Account")
|
||||
contact_id = contact.id
|
||||
else:
|
||||
assert isinstance(contact, int)
|
||||
contact_id = contact
|
||||
chat_id = lib.dc_create_chat_by_contact_id(self._dc_context, contact_id)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def _create_chat_by_message_id(self, msg_id):
|
||||
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
|
||||
def create_chat_by_message(self, message):
|
||||
""" create or get an existing chat object for the
|
||||
the specified message.
|
||||
|
||||
def create_group_chat(self, name, contacts=None, verified=False):
|
||||
:param message: messsage id or message instance.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
if hasattr(message, "id"):
|
||||
if self._dc_context != message._dc_context:
|
||||
raise ValueError("Message belongs to a different Account")
|
||||
msg_id = message.id
|
||||
else:
|
||||
assert isinstance(message, int)
|
||||
msg_id = message
|
||||
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def create_group_chat(self, name, verified=False):
|
||||
""" create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
:param contacts: list of contacts to add
|
||||
:param verified: if true only verified contacts can be added.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
bytes_name = name.encode("utf8")
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||
chat = Chat(self, chat_id)
|
||||
if contacts is not None:
|
||||
for contact in contacts:
|
||||
chat.add_contact(contact)
|
||||
return chat
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def get_chats(self):
|
||||
""" return list of chats.
|
||||
@@ -399,18 +368,13 @@ 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.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
""" export public and private keys to the specified directory. """
|
||||
return self._export(path, imex_cmd=1)
|
||||
|
||||
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 does not have to be started.
|
||||
"""
|
||||
export_files = self._export(path, 11)
|
||||
if len(export_files) != 1:
|
||||
@@ -418,16 +382,16 @@ class Account(object):
|
||||
return export_files[0]
|
||||
|
||||
def _export(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
return imex_tracker.wait_finish()
|
||||
|
||||
def import_self_keys(self, path):
|
||||
""" 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.
|
||||
|
||||
Note that the account does not have to be started.
|
||||
"""
|
||||
self._import(path, imex_cmd=2)
|
||||
|
||||
@@ -440,8 +404,10 @@ class Account(object):
|
||||
self._import(path, imex_cmd=12)
|
||||
|
||||
def _import(self, path, imex_cmd):
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
imex_tracker.wait_finish()
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
@@ -450,8 +416,8 @@ class Account(object):
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
"""
|
||||
self.check_is_configured()
|
||||
if not self.is_started():
|
||||
raise RuntimeError("IO not running, can not send out")
|
||||
if not self._threads.is_started():
|
||||
raise RuntimeError("threads not running, can not send out")
|
||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||
if res == ffi.NULL:
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
@@ -507,6 +473,46 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def stop_ongoing(self):
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
#
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
return self.get_message_by_id(ev[2])
|
||||
|
||||
def start_threads(self, mvbox=False, sentbox=False):
|
||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
||||
|
||||
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
||||
:returns: None
|
||||
"""
|
||||
if not self.is_configured():
|
||||
self.configure()
|
||||
self._threads.start(mvbox=mvbox, sentbox=sentbox)
|
||||
|
||||
def stop_threads(self, wait=True):
|
||||
""" stop IMAP/SMTP threads. """
|
||||
if self._threads.is_started():
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=wait)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
||||
# print("SHUTDOWN", self)
|
||||
self.stop_threads(wait=False)
|
||||
lib.dc_close(self._dc_context)
|
||||
self.stop_threads(wait=wait) # to wait for threads
|
||||
deltachat.clear_context_callback(self._dc_context)
|
||||
del self._dc_context
|
||||
atexit.unregister(self.shutdown)
|
||||
self.pluggy.unregister(self._evlogger)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
@@ -521,104 +527,131 @@ class Account(object):
|
||||
if dc_res == 0:
|
||||
raise ValueError("no chat is streaming locations")
|
||||
|
||||
#
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def add_account_plugin(self, plugin, name=None):
|
||||
""" add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
"""
|
||||
self._pm.register(plugin, name=name)
|
||||
self._pm.check_pending()
|
||||
return plugin
|
||||
class ImexTracker:
|
||||
def __init__(self, account):
|
||||
self._imex_events = Queue()
|
||||
self.account = account
|
||||
|
||||
def remove_account_plugin(self, plugin, name=None):
|
||||
""" remove an account plugin. """
|
||||
self._pm.unregister(plugin, name=name)
|
||||
def __enter__(self):
|
||||
self.account.pluggy.register(self)
|
||||
return self
|
||||
|
||||
@contextmanager
|
||||
def temp_plugin(self, plugin):
|
||||
""" run a with-block with the given plugin temporarily registered. """
|
||||
self._pm.register(plugin)
|
||||
yield plugin
|
||||
self._pm.unregister(plugin)
|
||||
def __exit__(self, *args):
|
||||
self.account.pluggy.unregister(self)
|
||||
|
||||
def stop_ongoing(self):
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
# there could be multiple accounts instantiated
|
||||
if self.account is not account:
|
||||
return
|
||||
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
||||
self._imex_events.put(data1)
|
||||
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(data1)
|
||||
|
||||
def start_io(self):
|
||||
""" start this account's IO scheduling (Rust-core async scheduler)
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif ev == 0:
|
||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||
elif ev == 1000:
|
||||
return files_written
|
||||
|
||||
If this account is not configured an Exception is raised.
|
||||
You need to call account.configure() and account.wait_configure_finish()
|
||||
before.
|
||||
|
||||
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
|
||||
account is started.
|
||||
|
||||
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
||||
:raises ConfigureFailed: if the account could not be configured.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
if not self.is_configured():
|
||||
raise ValueError("account not configured, cannot start io")
|
||||
lib.dc_start_io(self._dc_context)
|
||||
|
||||
def configure(self):
|
||||
""" 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.
|
||||
"""
|
||||
assert not self.is_configured()
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
configtracker = ConfigureTracker(self)
|
||||
self.add_account_plugin(configtracker)
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
class IOThreads:
|
||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||
self._dc_context = dc_context
|
||||
self._thread_quitflag = False
|
||||
self._name2thread = {}
|
||||
self._log_event = log_event
|
||||
|
||||
def is_started(self):
|
||||
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
|
||||
return len(self._name2thread) > 0
|
||||
|
||||
def wait_shutdown(self):
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
||||
assert not self.is_started()
|
||||
if imap:
|
||||
self._start_one_thread("inbox", self.imap_thread_run)
|
||||
if mvbox:
|
||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||
if sentbox:
|
||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||
if smtp:
|
||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||
|
||||
def stop_io(self):
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
def _start_one_thread(self, name, func):
|
||||
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
if bool(lib.dc_is_io_running(self._dc_context)):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
else:
|
||||
self.log("stop_scheduler called on non-running context")
|
||||
def stop(self, wait=False):
|
||||
self._thread_quitflag = True
|
||||
|
||||
def shutdown(self):
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
return
|
||||
# Workaround for a race condition. Make sure that thread is
|
||||
# not in between checking for quitflag and entering idle.
|
||||
time.sleep(0.5)
|
||||
|
||||
self.stop_io()
|
||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||
if wait:
|
||||
for name, thread in self._name2thread.items():
|
||||
thread.join()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
# the dc_context_unref triggers get_next_event to return ffi.NULL
|
||||
# which in turns makes the event thread finish execution
|
||||
self._dc_context = None
|
||||
def imap_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
self._event_thread.wait()
|
||||
def mvbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
||||
|
||||
self._shutdown_event.set()
|
||||
def sentbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
||||
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_after_shutdown(account=self)
|
||||
self.log("shutdown finished")
|
||||
def smtp_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||
|
||||
|
||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||
# destructor for dc_context
|
||||
dc_context_unref(dc_context)
|
||||
try:
|
||||
deltachat.clear_context_callback(dc_context)
|
||||
except (TypeError, AttributeError):
|
||||
# we are deep into Python Interpreter shutdown,
|
||||
# so no need to clear the callback context mapping.
|
||||
pass
|
||||
|
||||
|
||||
class ScannedQRCode:
|
||||
|
||||
@@ -18,25 +18,24 @@ class Chat(object):
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
from .account import Account
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self._dc_context = account._dc_context
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
self._dc_context == getattr(other, "_dc_context", None)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
|
||||
|
||||
@property
|
||||
def _dc_chat(self):
|
||||
return ffi.gc(
|
||||
lib.dc_get_chat(self.account._dc_context, self.id),
|
||||
lib.dc_get_chat(self._dc_context, self.id),
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
@@ -48,20 +47,10 @@ class Chat(object):
|
||||
- does not delete messages on server
|
||||
- the chat or contact is not blocked, new message will arrive
|
||||
"""
|
||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||
lib.dc_delete_chat(self._dc_context, self.id)
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
: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) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||
)
|
||||
|
||||
def is_deaddrop(self):
|
||||
""" return true if this chat is a deaddrop chat.
|
||||
|
||||
@@ -106,7 +95,7 @@ class Chat(object):
|
||||
:returns: None
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||
return lib.dc_set_chat_name(self._dc_context, self.id, name)
|
||||
|
||||
def mute(self, duration=None):
|
||||
""" mutes the chat
|
||||
@@ -118,7 +107,7 @@ class Chat(object):
|
||||
mute_duration = -1
|
||||
else:
|
||||
mute_duration = duration
|
||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
|
||||
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
@@ -127,7 +116,7 @@ class Chat(object):
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0)
|
||||
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
@@ -140,7 +129,7 @@ class Chat(object):
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
|
||||
def get_type(self):
|
||||
""" (deprecated) return type of this chat.
|
||||
""" return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
@@ -153,7 +142,7 @@ class Chat(object):
|
||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||
where account.join_with_qrcode(qr) needs to be called.
|
||||
"""
|
||||
res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id)
|
||||
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
@@ -175,7 +164,7 @@ class Chat(object):
|
||||
assert msg.id != 0
|
||||
# get a fresh copy of dc_msg, the core needs it
|
||||
msg = Message.from_db(self.account, msg.id)
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
# modify message in place to avoid bad state for the caller
|
||||
@@ -190,7 +179,7 @@ class Chat(object):
|
||||
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||
"""
|
||||
msg = as_dc_charpointer(text)
|
||||
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
||||
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be send, does chat exist?")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
@@ -205,7 +194,7 @@ class Chat(object):
|
||||
"""
|
||||
msg = Message.new_empty(self.account, view_type="file")
|
||||
msg.set_file(path, mime_type)
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
return Message.from_db(self.account, sent_id)
|
||||
@@ -220,7 +209,7 @@ class Chat(object):
|
||||
mime_type = mimetypes.guess_type(path)[0]
|
||||
msg = Message.new_empty(self.account, view_type="image")
|
||||
msg.set_file(path, mime_type)
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
return Message.from_db(self.account, sent_id)
|
||||
@@ -231,7 +220,7 @@ class Chat(object):
|
||||
:param msg: the message to be prepared.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be prepared")
|
||||
# invalidate passed in message which is not safe to use anymore
|
||||
@@ -267,7 +256,7 @@ class Chat(object):
|
||||
msg = Message.from_db(self.account, message.id)
|
||||
|
||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
||||
sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
assert sent_id == msg.id
|
||||
@@ -281,9 +270,9 @@ class Chat(object):
|
||||
:returns: None
|
||||
"""
|
||||
if message is None:
|
||||
lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL)
|
||||
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL)
|
||||
else:
|
||||
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
|
||||
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg)
|
||||
|
||||
def get_draft(self):
|
||||
""" get draft message for this chat.
|
||||
@@ -291,7 +280,7 @@ class Chat(object):
|
||||
:param message: a :class:`Message` instance
|
||||
:returns: Message object or None (if no draft available)
|
||||
"""
|
||||
x = lib.dc_get_draft(self.account._dc_context, self.id)
|
||||
x = lib.dc_get_draft(self._dc_context, self.id)
|
||||
if x == ffi.NULL:
|
||||
return None
|
||||
dc_msg = ffi.gc(x, lib.dc_msg_unref)
|
||||
@@ -303,7 +292,7 @@ class Chat(object):
|
||||
: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),
|
||||
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
||||
@@ -313,69 +302,60 @@ class Chat(object):
|
||||
|
||||
:returns: number of fresh messages
|
||||
"""
|
||||
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
|
||||
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id)
|
||||
|
||||
def mark_noticed(self):
|
||||
""" 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)
|
||||
return lib.dc_marknoticed_chat(self._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)
|
||||
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
|
||||
s = from_dc_charpointer(dc_res)
|
||||
return json.loads(s)
|
||||
|
||||
# ------ group management API ------------------------------
|
||||
|
||||
def add_contact(self, obj):
|
||||
def add_contact(self, contact):
|
||||
""" add a contact to this chat.
|
||||
|
||||
:params obj: Contact, Account or e-mail address.
|
||||
:params: contact object.
|
||||
:raises ValueError: if contact could not be added
|
||||
:returns: None
|
||||
"""
|
||||
contact = self.account.create_contact(obj)
|
||||
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
|
||||
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id)
|
||||
if ret != 1:
|
||||
raise ValueError("could not add contact {!r} to chat".format(contact))
|
||||
return contact
|
||||
|
||||
def remove_contact(self, obj):
|
||||
def remove_contact(self, contact):
|
||||
""" remove a contact from this chat.
|
||||
|
||||
:params obj: Contact, Account or e-mail address.
|
||||
:params: contact object.
|
||||
:raises ValueError: if contact could not be removed
|
||||
:returns: None
|
||||
"""
|
||||
contact = self.account.create_contact(obj)
|
||||
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
|
||||
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id)
|
||||
if ret != 1:
|
||||
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||
|
||||
def get_contacts(self):
|
||||
""" get all contacts for this chat.
|
||||
:params: contact object.
|
||||
: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),
|
||||
lib.dc_get_chat_contacts(self._dc_context, self.id),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return list(iter_array(
|
||||
dc_array, lambda id: Contact(self.account, id))
|
||||
dc_array, lambda id: Contact(self._dc_context, id))
|
||||
)
|
||||
|
||||
def num_contacts(self):
|
||||
""" return number of contacts in this chat. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
||||
lib.dc_array_unref
|
||||
)
|
||||
return lib.dc_array_get_cnt(dc_array)
|
||||
|
||||
def set_profile_image(self, img_path):
|
||||
"""Set group profile image.
|
||||
|
||||
@@ -388,7 +368,7 @@ class Chat(object):
|
||||
"""
|
||||
assert os.path.exists(img_path), img_path
|
||||
p = as_dc_charpointer(img_path)
|
||||
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
|
||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
|
||||
if res != 1:
|
||||
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
||||
|
||||
@@ -401,7 +381,7 @@ class Chat(object):
|
||||
:raises ValueError: if profile image could not be reset
|
||||
:returns: None
|
||||
"""
|
||||
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL)
|
||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
|
||||
if res != 1:
|
||||
raise ValueError("Removing Profile Image failed")
|
||||
|
||||
@@ -425,13 +405,19 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
def get_subtitle(self):
|
||||
"""return the subtitle of the chat
|
||||
:returns: the subtitle
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
||||
|
||||
# ------ location streaming API ------------------------------
|
||||
|
||||
def is_sending_locations(self):
|
||||
"""return True if this chat has location-sending enabled currently.
|
||||
:returns: True if location sending is enabled.
|
||||
"""
|
||||
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
|
||||
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
||||
|
||||
def is_archived(self):
|
||||
"""return True if this chat is archived.
|
||||
@@ -444,7 +430,7 @@ class Chat(object):
|
||||
|
||||
all subsequent messages will carry a location with them.
|
||||
"""
|
||||
lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds)
|
||||
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
|
||||
|
||||
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
||||
"""return list of locations for the given contact in the given timespan.
|
||||
@@ -468,7 +454,7 @@ class Chat(object):
|
||||
else:
|
||||
contact_id = contact.id
|
||||
|
||||
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
|
||||
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
|
||||
return [
|
||||
Location(
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
|
||||
@@ -1,7 +1,200 @@
|
||||
from .capi import lib
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from os.path import dirname, abspath
|
||||
from os.path import join as joinpath
|
||||
|
||||
# the following const are generated from deltachat.h
|
||||
# this works well when you in a git-checkout
|
||||
# run "python deltachat/const.py" to regenerate events
|
||||
# begin const generated
|
||||
DC_GCL_ARCHIVED_ONLY = 0x01
|
||||
DC_GCL_NO_SPECIALS = 0x02
|
||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||
DC_GCL_VERIFIED_ONLY = 0x01
|
||||
DC_GCL_ADD_SELF = 0x02
|
||||
DC_QR_ASK_VERIFYCONTACT = 200
|
||||
DC_QR_ASK_VERIFYGROUP = 202
|
||||
DC_QR_FPR_OK = 210
|
||||
DC_QR_FPR_MISMATCH = 220
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230
|
||||
DC_QR_ACCOUNT = 250
|
||||
DC_QR_ADDR = 320
|
||||
DC_QR_TEXT = 330
|
||||
DC_QR_URL = 332
|
||||
DC_QR_ERROR = 400
|
||||
DC_CHAT_ID_DEADDROP = 1
|
||||
DC_CHAT_ID_TRASH = 3
|
||||
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
||||
DC_CHAT_ID_STARRED = 5
|
||||
DC_CHAT_ID_ARCHIVED_LINK = 6
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7
|
||||
DC_CHAT_ID_LAST_SPECIAL = 9
|
||||
DC_CHAT_TYPE_UNDEFINED = 0
|
||||
DC_CHAT_TYPE_SINGLE = 100
|
||||
DC_CHAT_TYPE_GROUP = 120
|
||||
DC_CHAT_TYPE_VERIFIED_GROUP = 130
|
||||
DC_MSG_ID_MARKER1 = 1
|
||||
DC_MSG_ID_DAYMARKER = 9
|
||||
DC_MSG_ID_LAST_SPECIAL = 9
|
||||
DC_STATE_UNDEFINED = 0
|
||||
DC_STATE_IN_FRESH = 10
|
||||
DC_STATE_IN_NOTICED = 13
|
||||
DC_STATE_IN_SEEN = 16
|
||||
DC_STATE_OUT_PREPARING = 18
|
||||
DC_STATE_OUT_DRAFT = 19
|
||||
DC_STATE_OUT_PENDING = 20
|
||||
DC_STATE_OUT_FAILED = 24
|
||||
DC_STATE_OUT_DELIVERED = 26
|
||||
DC_STATE_OUT_MDN_RCVD = 28
|
||||
DC_CONTACT_ID_SELF = 1
|
||||
DC_CONTACT_ID_INFO = 2
|
||||
DC_CONTACT_ID_DEVICE = 5
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9
|
||||
DC_MSG_TEXT = 10
|
||||
DC_MSG_IMAGE = 20
|
||||
DC_MSG_GIF = 21
|
||||
DC_MSG_STICKER = 23
|
||||
DC_MSG_AUDIO = 40
|
||||
DC_MSG_VOICE = 41
|
||||
DC_MSG_VIDEO = 50
|
||||
DC_MSG_FILE = 60
|
||||
DC_LP_AUTH_OAUTH2 = 0x2
|
||||
DC_LP_AUTH_NORMAL = 0x4
|
||||
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
|
||||
DC_LP_IMAP_SOCKET_SSL = 0x200
|
||||
DC_LP_IMAP_SOCKET_PLAIN = 0x400
|
||||
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
|
||||
DC_LP_SMTP_SOCKET_SSL = 0x20000
|
||||
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
|
||||
DC_CERTCK_AUTO = 0
|
||||
DC_CERTCK_STRICT = 1
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
|
||||
DC_EMPTY_MVBOX = 0x01
|
||||
DC_EMPTY_INBOX = 0x02
|
||||
DC_EVENT_INFO = 100
|
||||
DC_EVENT_SMTP_CONNECTED = 101
|
||||
DC_EVENT_IMAP_CONNECTED = 102
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105
|
||||
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
|
||||
DC_EVENT_NEW_BLOB_FILE = 150
|
||||
DC_EVENT_DELETED_BLOB_FILE = 151
|
||||
DC_EVENT_WARNING = 300
|
||||
DC_EVENT_ERROR = 400
|
||||
DC_EVENT_ERROR_NETWORK = 401
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410
|
||||
DC_EVENT_MSGS_CHANGED = 2000
|
||||
DC_EVENT_INCOMING_MSG = 2005
|
||||
DC_EVENT_MSG_DELIVERED = 2010
|
||||
DC_EVENT_MSG_FAILED = 2012
|
||||
DC_EVENT_MSG_READ = 2015
|
||||
DC_EVENT_CHAT_MODIFIED = 2020
|
||||
DC_EVENT_CONTACTS_CHANGED = 2030
|
||||
DC_EVENT_LOCATION_CHANGED = 2035
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041
|
||||
DC_EVENT_IMEX_PROGRESS = 2051
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
||||
DC_EVENT_FILE_COPIED = 2055
|
||||
DC_EVENT_IS_OFFLINE = 2081
|
||||
DC_EVENT_GET_STRING = 2091
|
||||
DC_STR_SELFNOTINGRP = 21
|
||||
DC_KEY_GEN_DEFAULT = 0
|
||||
DC_KEY_GEN_RSA2048 = 1
|
||||
DC_KEY_GEN_ED25519 = 2
|
||||
DC_PROVIDER_STATUS_OK = 1
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2
|
||||
DC_PROVIDER_STATUS_BROKEN = 3
|
||||
DC_CHAT_VISIBILITY_NORMAL = 0
|
||||
DC_CHAT_VISIBILITY_ARCHIVED = 1
|
||||
DC_CHAT_VISIBILITY_PINNED = 2
|
||||
DC_STR_NOMESSAGES = 1
|
||||
DC_STR_SELF = 2
|
||||
DC_STR_DRAFT = 3
|
||||
DC_STR_MEMBER = 4
|
||||
DC_STR_CONTACT = 6
|
||||
DC_STR_VOICEMESSAGE = 7
|
||||
DC_STR_DEADDROP = 8
|
||||
DC_STR_IMAGE = 9
|
||||
DC_STR_VIDEO = 10
|
||||
DC_STR_AUDIO = 11
|
||||
DC_STR_FILE = 12
|
||||
DC_STR_STATUSLINE = 13
|
||||
DC_STR_NEWGROUPDRAFT = 14
|
||||
DC_STR_MSGGRPNAME = 15
|
||||
DC_STR_MSGGRPIMGCHANGED = 16
|
||||
DC_STR_MSGADDMEMBER = 17
|
||||
DC_STR_MSGDELMEMBER = 18
|
||||
DC_STR_MSGGROUPLEFT = 19
|
||||
DC_STR_GIF = 23
|
||||
DC_STR_ENCRYPTEDMSG = 24
|
||||
DC_STR_E2E_AVAILABLE = 25
|
||||
DC_STR_ENCR_TRANSP = 27
|
||||
DC_STR_ENCR_NONE = 28
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29
|
||||
DC_STR_FINGERPRINTS = 30
|
||||
DC_STR_READRCPT = 31
|
||||
DC_STR_READRCPT_MAILBODY = 32
|
||||
DC_STR_MSGGRPIMGDELETED = 33
|
||||
DC_STR_E2E_PREFERRED = 34
|
||||
DC_STR_CONTACT_VERIFIED = 35
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37
|
||||
DC_STR_ARCHIVEDCHATS = 40
|
||||
DC_STR_STARREDMSGS = 41
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43
|
||||
DC_STR_SELFTALK_SUBTITLE = 50
|
||||
DC_STR_CANNOT_LOGIN = 60
|
||||
DC_STR_SERVER_RESPONSE = 61
|
||||
DC_STR_MSGACTIONBYUSER = 62
|
||||
DC_STR_MSGACTIONBYME = 63
|
||||
DC_STR_MSGLOCATIONENABLED = 64
|
||||
DC_STR_MSGLOCATIONDISABLED = 65
|
||||
DC_STR_LOCATION = 66
|
||||
DC_STR_STICKER = 67
|
||||
DC_STR_DEVICE_MESSAGES = 68
|
||||
DC_STR_COUNT = 68
|
||||
# end const generated
|
||||
|
||||
|
||||
for name in dir(lib):
|
||||
if name.startswith("DC_"):
|
||||
globals()[name] = getattr(lib, name)
|
||||
del name
|
||||
def read_event_defines(f):
|
||||
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
|
||||
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
|
||||
for line in f:
|
||||
m = rex.match(line)
|
||||
if m:
|
||||
yield m.groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
here = abspath(__file__).rstrip("oc")
|
||||
here_dir = dirname(here)
|
||||
if len(sys.argv) >= 2:
|
||||
deltah = sys.argv[1]
|
||||
else:
|
||||
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
|
||||
assert os.path.exists(deltah)
|
||||
|
||||
lines = []
|
||||
skip_to_end = False
|
||||
for orig_line in open(here):
|
||||
if skip_to_end:
|
||||
if not orig_line.startswith("# end const"):
|
||||
continue
|
||||
skip_to_end = False
|
||||
lines.append(orig_line)
|
||||
if orig_line.startswith("# begin const"):
|
||||
with open(deltah) as f:
|
||||
for name, item in read_event_defines(f):
|
||||
lines.append("{} = {}\n".format(name, item))
|
||||
skip_to_end = True
|
||||
|
||||
tmpname = here + ".tmp"
|
||||
with open(tmpname, "w") as f:
|
||||
f.write("".join(lines))
|
||||
os.rename(tmpname, here)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -12,25 +10,23 @@ class Contact(object):
|
||||
|
||||
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
|
||||
def __init__(self, dc_context, id):
|
||||
self._dc_context = dc_context
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||
return self._dc_context == other._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
||||
|
||||
@property
|
||||
def _dc_contact(self):
|
||||
return ffi.gc(
|
||||
lib.dc_get_contact(self.account._dc_context, self.id),
|
||||
lib.dc_get_contact(self._dc_context, self.id),
|
||||
lib.dc_contact_unref
|
||||
)
|
||||
|
||||
@@ -40,13 +36,10 @@ class Contact(object):
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def display_name(self):
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -64,17 +57,3 @@ class Contact(object):
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
def create_chat(self):
|
||||
""" 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.
|
||||
"""
|
||||
dc_context = self.account._dc_context
|
||||
chat_id = lib.dc_create_chat_by_contact_id(dc_context, self.id)
|
||||
assert chat_id > const.DC_CHAT_ID_LAST_SPECIAL, chat_id
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
# deprecated name
|
||||
get_chat = create_chat
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the testplugin
|
||||
and for cleaning up inbox/mvbox for each test function run.
|
||||
"""
|
||||
|
||||
import io
|
||||
import email
|
||||
import ssl
|
||||
import pathlib
|
||||
from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import deltachat
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
DELETED = b'\\Deleted'
|
||||
FLAGS = b'FLAGS'
|
||||
FETCH = b'FETCH'
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_extra_configure(account):
|
||||
""" Reset the account (we reuse accounts across tests)
|
||||
and make 'account.direct_imap' available for direct IMAP ops.
|
||||
"""
|
||||
imap = DirectImap(account)
|
||||
if imap.select_config_folder("mvbox"):
|
||||
imap.delete(ALL, expunge=True)
|
||||
assert imap.select_config_folder("inbox")
|
||||
imap.delete(ALL, expunge=True)
|
||||
setattr(account, "direct_imap", imap)
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_after_shutdown(account):
|
||||
""" shutdown the imap connection if there is one. """
|
||||
imap = getattr(account, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.shutdown()
|
||||
del account.direct_imap
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
self.conn = IMAPClient(host, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.idle_done()
|
||||
except (OSError, IMAPClientError):
|
||||
pass
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, IMAPClientError):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def select_folder(self, foldername):
|
||||
assert not self._idling
|
||||
return self.conn.select_folder(foldername)
|
||||
|
||||
def select_config_folder(self, config_name):
|
||||
""" 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)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
|
||||
def list_folders(self):
|
||||
""" return list of all existing folder names"""
|
||||
assert not self._idling
|
||||
folders = []
|
||||
for meta, sep, foldername in self.conn.list_folders():
|
||||
folders.append(foldername)
|
||||
return folders
|
||||
|
||||
def delete(self, range, expunge=True):
|
||||
""" 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.set_flags(range, [DELETED])
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self):
|
||||
assert not self._idling
|
||||
return self.conn.fetch(ALL, [FLAGS])
|
||||
|
||||
def get_unread_messages(self):
|
||||
assert not self._idling
|
||||
res = self.conn.fetch(ALL, [FLAGS])
|
||||
return [uid for uid in res
|
||||
if SEEN not in res[uid][FLAGS]]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.set_flags(messages, [SEEN])
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self):
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_account_info(self, logfile):
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = logfile
|
||||
print(*args, **kwargs)
|
||||
|
||||
cursor = 0
|
||||
for name, val in self.account.get_info().items():
|
||||
entry = "{}={}".format(name.upper(), val)
|
||||
if cursor + len(entry) > 80:
|
||||
log("")
|
||||
cursor = 0
|
||||
log(entry, end=" ")
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
assert not self._idling
|
||||
stream = io.StringIO()
|
||||
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = stream
|
||||
print(*args, **kwargs)
|
||||
|
||||
empty_folders = []
|
||||
for imapfolder in self.list_folders():
|
||||
self.select_folder(imapfolder)
|
||||
messages = list(self.get_all_messages())
|
||||
if not messages:
|
||||
empty_folders.append(imapfolder)
|
||||
continue
|
||||
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
requested = [b'BODY.PEEK[HEADER]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[HEADER]']
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(uid))
|
||||
fn.write_bytes(body_bytes)
|
||||
log("Message", uid, fn)
|
||||
email_message = email.message_from_bytes(body_bytes)
|
||||
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
def idle_start(self):
|
||||
""" switch this connection to idle mode. non-blocking. """
|
||||
assert not self._idling
|
||||
res = self.conn.idle()
|
||||
self._idling = True
|
||||
return res
|
||||
|
||||
def idle_check(self, terminate=False):
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
assert self._idling
|
||||
self.account.log("imap-direct: calling idle_check")
|
||||
res = self.conn.idle_check()
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
return res
|
||||
|
||||
def idle_wait_for_seen(self):
|
||||
""" Return first message with SEEN flag
|
||||
from a running idle-stream REtiurn.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.idle_check():
|
||||
if item[1] == FETCH:
|
||||
if item[2][0] == FLAGS:
|
||||
if SEEN in item[2][1]:
|
||||
return item[0]
|
||||
|
||||
def idle_done(self):
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
if self._idling:
|
||||
res = self.conn.idle_done()
|
||||
self._idling = False
|
||||
return res
|
||||
81
python/src/deltachat/eventlogger.py
Normal file
81
python/src/deltachat/eventlogger.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import threading
|
||||
import re
|
||||
import time
|
||||
from queue import Queue, Empty
|
||||
from .hookspec import hookimpl
|
||||
|
||||
|
||||
class EventLogger:
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account, logid=None, debug=True):
|
||||
self.account = account
|
||||
self._event_queue = Queue()
|
||||
self._debug = debug
|
||||
if logid is None:
|
||||
logid = str(self.account._dc_context).strip(">").split()[-1]
|
||||
self.logid = logid
|
||||
self._timeout = None
|
||||
self.init_time = time.time()
|
||||
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
if self.account == account:
|
||||
self._log_event(event_name, data1, data2)
|
||||
self._event_queue.put((event_name, data1, data2))
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
def consume_events(self, check_error=True):
|
||||
while not self._event_queue.empty():
|
||||
self.get(check_error=check_error)
|
||||
|
||||
def get(self, timeout=None, check_error=True):
|
||||
timeout = timeout or self._timeout
|
||||
ev = self._event_queue.get(timeout=timeout)
|
||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
try:
|
||||
ev = self._event_queue.get(False)
|
||||
except Empty:
|
||||
break
|
||||
else:
|
||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
ev = self.get(timeout=timeout, check_error=check_error)
|
||||
if rex.match(ev[0]):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev[2]):
|
||||
return ev
|
||||
|
||||
def _log_event(self, evt_name, data1, data2):
|
||||
# don't show events that are anyway empty impls now
|
||||
if evt_name == "DC_EVENT_GET_STRING":
|
||||
return
|
||||
if self._debug:
|
||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||
self._log(evpart)
|
||||
|
||||
def _log(self, msg):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
with self._loglock:
|
||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||
@@ -1,219 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
from queue import Queue, Empty
|
||||
|
||||
import deltachat
|
||||
from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
|
||||
def __str__(self):
|
||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||
|
||||
|
||||
class FFIEventLogger:
|
||||
""" 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()
|
||||
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self.account.log(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
elapsed = time.time() - self.init_time
|
||||
locname = tname
|
||||
if self.logid:
|
||||
locname += "-" + self.logid
|
||||
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||
with self._loglock:
|
||||
print(s, flush=True)
|
||||
|
||||
|
||||
class FFIEventTracker:
|
||||
def __init__(self, account, timeout=None):
|
||||
self.account = account
|
||||
self._timeout = timeout
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
def consume_events(self, check_error=True):
|
||||
while not self._event_queue.empty():
|
||||
self.get(check_error=check_error)
|
||||
|
||||
def get(self, timeout=None, check_error=True):
|
||||
timeout = timeout if timeout is not None else self._timeout
|
||||
ev = self._event_queue.get(timeout=timeout)
|
||||
if check_error and ev.name == "DC_EVENT_ERROR":
|
||||
raise ValueError("unexpected event: {}".format(ev))
|
||||
return ev
|
||||
|
||||
def iter_events(self, timeout=None, check_error=True):
|
||||
while 1:
|
||||
yield self.get(timeout=timeout, check_error=check_error)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
for ev in self.iter_events(timeout=timeout, check_error=check_error):
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev.data2):
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
try:
|
||||
ev = self._event_queue.get(False)
|
||||
except Empty:
|
||||
break
|
||||
else:
|
||||
assert not rex.match(ev.name), "event found {}".format(ev)
|
||||
|
||||
def wait_securejoin_inviter_progress(self, target):
|
||||
while 1:
|
||||
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||
if event.data2 >= target:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
||||
break
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" 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
|
||||
if the event contains no msgid"""
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev.data1 == msg.chat.id
|
||||
assert ev.data2 == msg.id
|
||||
assert msg.is_out_delivered()
|
||||
|
||||
|
||||
class EventThread(threading.Thread):
|
||||
""" Event Thread for an account.
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
@contextmanager
|
||||
def log_execution(self, message):
|
||||
self.account.log(message + " START")
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def wait(self):
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
""" 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),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while 1:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL:
|
||||
break
|
||||
evt = lib.dc_event_get_id(event)
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
# the following code relates to the deltachat/_build.py's helper
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
lib.dc_event_unref(event)
|
||||
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||
try:
|
||||
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
|
||||
hook = getattr(self.account._pm.hook, name)
|
||||
hook(**kwargs)
|
||||
except Exception:
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
name = ffi_event.name
|
||||
account = self.account
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
data1 = ffi_event.data1
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
yield "ac_configure_completed", dict(success=success)
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg.is_outgoing():
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
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))
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", dict(message=msg)
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", dict(chat=chat)
|
||||
@@ -1,101 +1,25 @@
|
||||
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||
""" Hooks for python bindings """
|
||||
|
||||
import pluggy
|
||||
|
||||
name = "deltachat"
|
||||
|
||||
account_spec_name = "deltachat-account"
|
||||
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
||||
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
||||
|
||||
global_spec_name = "deltachat-global"
|
||||
global_hookspec = pluggy.HookspecMarker(global_spec_name)
|
||||
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
||||
hookspec = pluggy.HookspecMarker(name)
|
||||
hookimpl = pluggy.HookimplMarker(name)
|
||||
_plugin_manager = None
|
||||
|
||||
|
||||
class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
All hooks are executed in a dedicated Event thread.
|
||||
Hooks are 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)
|
||||
pm.add_hookspecs(cls)
|
||||
return pm
|
||||
|
||||
@account_hookspec
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
""" 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>`_.
|
||||
|
||||
DANGER: this hook is executed from the callback invoked by core.
|
||||
Hook implementations need to be short running and can typically
|
||||
not call back into core because this would easily cause recursion issues.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_log_line(self, message):
|
||||
""" log a message related to the account. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_configure_completed(self, success):
|
||||
""" Called after a configure process completed. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_incoming_message(self, message):
|
||||
""" Called on any incoming message (to deaddrop or chat). """
|
||||
|
||||
@account_hookspec
|
||||
def ac_outgoing_message(self, message):
|
||||
""" 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. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
""" Chat was created or modified regarding membership, avatar, title. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
""" Called for each contact added to an accepted chat. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
""" Called for each contact removed from a chat. """
|
||||
def get_plugin_manager():
|
||||
global _plugin_manager
|
||||
if _plugin_manager is None:
|
||||
_plugin_manager = pluggy.PluginManager(name)
|
||||
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
||||
return _plugin_manager
|
||||
|
||||
|
||||
class Global:
|
||||
""" global hook specifications using a per-process singleton
|
||||
plugin manager instance.
|
||||
class DeltaChatHookSpecs:
|
||||
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
||||
|
||||
"""
|
||||
_plugin_manager = None
|
||||
|
||||
@classmethod
|
||||
def _get_plugin_manager(cls):
|
||||
if cls._plugin_manager is None:
|
||||
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
|
||||
pm.add_hookspecs(cls)
|
||||
return cls._plugin_manager
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_init(self, account):
|
||||
""" called when `Account::__init__()` function starts executing. """
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_extra_configure(self, account):
|
||||
""" Called when account configuration successfully finished.
|
||||
|
||||
This hook can be used to perform extra work before
|
||||
ac_configure_completed is called.
|
||||
"""
|
||||
|
||||
@global_hookspec
|
||||
def dc_account_after_shutdown(self, account):
|
||||
""" Called after the account has been shutdown. """
|
||||
@hookspec
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
""" process a CFFI low level events for a given account. """
|
||||
|
||||
@@ -16,7 +16,8 @@ class Message(object):
|
||||
"""
|
||||
def __init__(self, account, dc_msg):
|
||||
self.account = account
|
||||
assert isinstance(self.account._dc_context, ffi.CData)
|
||||
self._dc_context = account._dc_context
|
||||
assert isinstance(self._dc_context, ffi.CData)
|
||||
assert isinstance(dc_msg, ffi.CData)
|
||||
assert dc_msg != ffi.NULL
|
||||
self._dc_msg = dc_msg
|
||||
@@ -27,11 +28,7 @@ class Message(object):
|
||||
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())
|
||||
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, account, id):
|
||||
@@ -53,20 +50,6 @@ class Message(object):
|
||||
lib.dc_msg_unref
|
||||
))
|
||||
|
||||
def create_chat(self):
|
||||
""" create or get an existing chat (group) object for this message.
|
||||
|
||||
If the message is a deaddrop contact request
|
||||
the sender will become an accepted contact.
|
||||
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
from .chat import Chat
|
||||
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
|
||||
ctx = self.account._dc_context
|
||||
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
@@ -98,10 +81,6 @@ class Message(object):
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def is_system_message(self):
|
||||
""" return True if this message is a system/info message. """
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
|
||||
def is_setup_message(self):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
@@ -123,12 +102,12 @@ class Message(object):
|
||||
|
||||
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))
|
||||
return from_dc_charpointer(lib.dc_get_msg_info(self._dc_context, self.id))
|
||||
|
||||
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._dc_context,
|
||||
self.id,
|
||||
as_dc_charpointer(setup_code)
|
||||
)
|
||||
@@ -163,7 +142,7 @@ 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)
|
||||
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id)
|
||||
if mime_headers:
|
||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
||||
if isinstance(s, bytes):
|
||||
@@ -180,13 +159,6 @@ class Message(object):
|
||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
def get_sender_chat(self):
|
||||
"""return the 1:1 chat with the sender of this message.
|
||||
|
||||
:returns: :class:`deltachat.chat.Chat` instance
|
||||
"""
|
||||
return self.get_sender_contact().get_chat()
|
||||
|
||||
def get_sender_contact(self):
|
||||
"""return the contact of who wrote the message.
|
||||
|
||||
@@ -194,7 +166,7 @@ class Message(object):
|
||||
"""
|
||||
from .contact import Contact
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self.account, contact_id)
|
||||
return Contact(self._dc_context, contact_id)
|
||||
|
||||
#
|
||||
# Message State query methods
|
||||
@@ -206,7 +178,7 @@ class Message(object):
|
||||
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),
|
||||
lib.dc_get_msg(self._dc_context, self.id),
|
||||
lib.dc_msg_unref
|
||||
)
|
||||
return lib.dc_msg_get_state(dc_msg)
|
||||
@@ -235,13 +207,6 @@ class Message(object):
|
||||
"""
|
||||
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||
|
||||
def is_outgoing(self):
|
||||
"""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)
|
||||
|
||||
def is_out_preparing(self):
|
||||
"""Return True if Message is outgoing, but its file is being prepared.
|
||||
"""
|
||||
@@ -304,10 +269,6 @@ class Message(object):
|
||||
""" 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. """
|
||||
self.account.mark_seen_messages([self.id])
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
@@ -327,29 +288,3 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
return code
|
||||
raise ValueError("message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
||||
|
||||
|
||||
#
|
||||
# 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)
|
||||
if res:
|
||||
contact = msg.account.get_contact_by_addr(res[1])
|
||||
if contact:
|
||||
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y removed by a@b
|
||||
text = text.lower()
|
||||
parts = text.split()
|
||||
if parts[0] == "member":
|
||||
if parts[2] in ("removed", "added"):
|
||||
return parts[2], parts[1]
|
||||
if parts[3] in ("removed", "added"):
|
||||
return parts[3], parts[2].strip("()")
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import subprocess
|
||||
import queue
|
||||
import threading
|
||||
import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from . import Account, const
|
||||
from .capi import lib
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
from _pytest._code import Source
|
||||
from deltachat import direct_imap
|
||||
|
||||
import deltachat
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
# Make sure we don't get garbled output because threads keep running
|
||||
# collect all ever created accounts in a weakref-set (so we don't
|
||||
# keep objects unneccessarily alive) and enable/disable logging
|
||||
# for each pytest test phase # (setup/call/teardown).
|
||||
# Additionally make the acfactory use a logging/no-logging default.
|
||||
|
||||
class LoggingAspect:
|
||||
def __init__(self):
|
||||
self._accounts = weakref.WeakSet()
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_init(self, account):
|
||||
self._accounts.add(account)
|
||||
|
||||
def disable_logging(self, item):
|
||||
for acc in self._accounts:
|
||||
acc.disable_logging()
|
||||
acfactory = item.funcargs.get("acfactory")
|
||||
if acfactory:
|
||||
acfactory.set_logging_default(False)
|
||||
|
||||
def enable_logging(self, item):
|
||||
for acc in self._accounts:
|
||||
acc.enable_logging()
|
||||
acfactory = item.funcargs.get("acfactory")
|
||||
if acfactory:
|
||||
acfactory.set_logging_default(True)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
if item.get_closest_marker("ignored"):
|
||||
if not item.config.getvalue("ignored"):
|
||||
pytest.skip("use --ignored to run this test")
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
self.disable_logging(item)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
self.enable_logging(pyfuncitem)
|
||||
yield
|
||||
self.disable_logging(pyfuncitem)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
self.disable_logging(item)
|
||||
|
||||
la = LoggingAspect()
|
||||
config.pluginmanager.register(la)
|
||||
deltachat.register_global_plugin(la)
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
try:
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
os.remove(t)
|
||||
summary.extend(['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))
|
||||
else:
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
# trying to find test data heuristically
|
||||
# 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(__file__), "..", "..", "..", "test-data")
|
||||
]]
|
||||
|
||||
def get_path(self, bn):
|
||||
""" 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):
|
||||
return fn
|
||||
print("WARNING: path does not exist: {!r}".format(fn))
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
if fn is not None:
|
||||
with open(fn, mode) as f:
|
||||
return f.read()
|
||||
|
||||
return Data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
deltachat.register_global_plugin(direct_imap)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
acc.shutdown()
|
||||
acc.disable_logging()
|
||||
deltachat.unregister_global_plugin(direct_imap)
|
||||
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
ac.add_account_plugin(FFIEventLogger(ac))
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
def set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
|
||||
def get_online_config(self, pre_generated_key=True, quiet=False):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
self.wait_configure_and_start_io()
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self, move=False, quiet=False):
|
||||
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
|
||||
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||
self.wait_configure_and_start_io()
|
||||
return ac1, ac2
|
||||
|
||||
def get_many_online_accounts(self, num, move=True):
|
||||
accounts = [self.get_online_configuring_account(move=move, quiet=True)
|
||||
for i in range(num)]
|
||||
self.wait_configure_and_start_io()
|
||||
for acc in accounts:
|
||||
acc.add_account_plugin(FFIEventLogger(acc))
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def wait_configure_and_start_io(self):
|
||||
for acc in self._accounts:
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
del acc._configtracker
|
||||
if acc.is_configured() and not acc.is_started():
|
||||
acc.start_io()
|
||||
print("{}: {} account was successfully setup".format(
|
||||
acc.get_config("displayname"), acc.get_config("addr")))
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_ac, bot_cfg = self.get_online_config()
|
||||
|
||||
# Avoid starting ac so we don't interfere with the bot operating on
|
||||
# the same database.
|
||||
self._accounts.remove(bot_ac)
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
args.insert(-1, "--show-ffi")
|
||||
print("$", " ".join(args))
|
||||
popen = subprocess.Popen(
|
||||
args=args,
|
||||
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
|
||||
)
|
||||
bot = BotProcess(popen, bot_cfg)
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_account_info(logfile=logfile)
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1, ac2):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
to_wait.append(acc2)
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
# we read stdout as quickly as we can in a thread and make
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
self.stdout_queue.put(line)
|
||||
print("bot-stdout: ", line)
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
|
||||
for next_pattern in patterns:
|
||||
print("+++FNMATCH:", next_pattern)
|
||||
ignored = []
|
||||
while 1:
|
||||
line = self.stdout_queue.get(timeout=15)
|
||||
if line is None:
|
||||
if ignored:
|
||||
print("BOT stdout terminated after these lines")
|
||||
for line in ignored:
|
||||
print(line)
|
||||
raise IOError("BOT stdout-thread terminated")
|
||||
if fnmatch.fnmatch(line, next_pattern):
|
||||
print("+++MATCHED:", line)
|
||||
break
|
||||
else:
|
||||
print("+++IGN:", line)
|
||||
ignored.append(line)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
return Printer()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
|
||||
# set a report attribute for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
|
||||
setattr(item, "rep_" + rep.when, rep)
|
||||
@@ -1,90 +0,0 @@
|
||||
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
|
||||
from .hookspec import account_hookimpl, Global
|
||||
|
||||
|
||||
class ImexFailed(RuntimeError):
|
||||
""" Exception for signalling that import/export operations failed."""
|
||||
|
||||
|
||||
class ImexTracker:
|
||||
def __init__(self):
|
||||
self._imex_events = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
|
||||
self._imex_events.put(ffi_event.data1)
|
||||
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(ffi_event.data2)
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif ev == 0:
|
||||
raise ImexFailed("export failed, exp-files: {}".format(files_written))
|
||||
elif ev == 1000:
|
||||
return files_written
|
||||
|
||||
|
||||
class ConfigureFailed(RuntimeError):
|
||||
""" Exception for signalling that configuration failed."""
|
||||
|
||||
|
||||
class ConfigureTracker:
|
||||
ConfigureFailed = ConfigureFailed
|
||||
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
self._configure_events = Queue()
|
||||
self._smtp_finished = Event()
|
||||
self._imap_finished = Event()
|
||||
self._ffi_events = []
|
||||
self._progress = Queue()
|
||||
self._gm = Global._get_plugin_manager()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self._ffi_events.append(ffi_event)
|
||||
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
|
||||
self._smtp_finished.set()
|
||||
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
|
||||
self._imap_finished.set()
|
||||
elif ffi_event.name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
self._progress.put(ffi_event.data1)
|
||||
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(self, success):
|
||||
if success:
|
||||
self._gm.hook.dc_account_extra_configure(account=self.account)
|
||||
self._configure_events.put(success)
|
||||
self.account.remove_account_plugin(self)
|
||||
|
||||
def wait_smtp_connected(self):
|
||||
""" wait until smtp is configured. """
|
||||
self._smtp_finished.wait()
|
||||
|
||||
def wait_imap_connected(self):
|
||||
""" wait until smtp is configured. """
|
||||
self._imap_finished.wait()
|
||||
|
||||
def wait_progress(self, data1=None):
|
||||
while 1:
|
||||
evdata = self._progress.get()
|
||||
if data1 is None or evdata == data1:
|
||||
break
|
||||
|
||||
def wait_finish(self):
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
if not self._configure_events.get():
|
||||
content = "\n".join(map(str, self._ffi_events))
|
||||
raise ConfigureFailed(content)
|
||||
@@ -10,6 +10,4 @@ if __name__ == "__main__":
|
||||
for relpath in os.listdir(workspacedir):
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
subprocess.check_call(
|
||||
["auditwheel", "repair", p, "-w", workspacedir,
|
||||
"--plat", "manylinux2014_x86_64"])
|
||||
subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir])
|
||||
|
||||
317
python/tests/conftest.py
Normal file
317
python/tests/conftest.py
Normal file
@@ -0,0 +1,317 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import py
|
||||
import pytest
|
||||
import requests
|
||||
import time
|
||||
from deltachat import Account
|
||||
from deltachat import const
|
||||
from deltachat.capi import lib
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import tempfile
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
||||
)
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if (list(item.iter_markers(name="ignored"))
|
||||
and not item.config.getoption("ignored")):
|
||||
pytest.skip("Ignored tests not requested, use --ignored")
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
m = MonkeyPatch()
|
||||
try:
|
||||
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
m.undo()
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
)])
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "#" in cfg:
|
||||
url, token = cfg.split("#", 1)
|
||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
||||
else:
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def data():
|
||||
class Data:
|
||||
def __init__(self):
|
||||
self.path = os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
def get_path(self, bn):
|
||||
fn = os.path.join(self.path, bn)
|
||||
assert os.path.exists(fn)
|
||||
return fn
|
||||
return Data()
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@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')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
def make_account(self, path, logid):
|
||||
ac = Account(path, logid=logid)
|
||||
self._finalizers.append(ac.shutdown)
|
||||
return ac
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = "key/{name}-public.asc".format(name=keyname)
|
||||
fname_sec = "key/{name}-secret.asc".format(name=keyname)
|
||||
account._preconfigure_keypair(addr,
|
||||
datadir.join(fname_pub).read(),
|
||||
datadir.join(fname_sec).read())
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
|
||||
def get_online_config(self, pre_generated_key=True):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False,
|
||||
pre_generated_key=True, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key)
|
||||
configdict.update(config)
|
||||
ac.configure(**configdict)
|
||||
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key)
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self):
|
||||
ac1 = self.get_online_configuring_account()
|
||||
ac2 = self.get_online_configuring_account()
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_successful_IMAP_SMTP_connection(ac2)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
return ac1, ac2
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
||||
ac.start_threads()
|
||||
return ac
|
||||
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
return am
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
return Printer()
|
||||
|
||||
|
||||
def wait_configuration_progress(account, min_target, max_target=1001):
|
||||
min_target = min(min_target, max_target)
|
||||
while 1:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if data1 >= min_target and data1 <= max_target:
|
||||
print("** CONFIG PROGRESS {}".format(min_target), account)
|
||||
break
|
||||
|
||||
|
||||
def wait_securejoin_inviter_progress(account, target):
|
||||
while 1:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||
if data2 >= target:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
||||
break
|
||||
|
||||
|
||||
def wait_successful_IMAP_SMTP_connection(account):
|
||||
imap_ok = smtp_ok = False
|
||||
while not imap_ok or not smtp_ok:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
|
||||
if evt_name == "DC_EVENT_IMAP_CONNECTED":
|
||||
imap_ok = True
|
||||
print("** IMAP OK", account)
|
||||
if evt_name == "DC_EVENT_SMTP_CONNECTED":
|
||||
smtp_ok = True
|
||||
print("** SMTP OK", account)
|
||||
print("** IMAP and SMTP logins successful", account)
|
||||
|
||||
|
||||
def wait_msgs_changed(account, chat_id, msg_id=None):
|
||||
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[1] == chat_id
|
||||
if msg_id is not None:
|
||||
assert ev[2] == msg_id
|
||||
return ev[2]
|
||||
@@ -1 +0,0 @@
|
||||
../../../test-data/key
|
||||
@@ -1,135 +0,0 @@
|
||||
|
||||
import time
|
||||
import threading
|
||||
import pytest
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
|
||||
import deltachat
|
||||
|
||||
|
||||
def test_db_busy_error(acfactory, tmpdir):
|
||||
starttime = time.time()
|
||||
log_lock = threading.RLock()
|
||||
|
||||
def log(string):
|
||||
with log_lock:
|
||||
print("%3.2f %s" % (time.time() - starttime, string))
|
||||
|
||||
# make a number of accounts
|
||||
accounts = acfactory.get_many_online_accounts(3, quiet=True)
|
||||
log("created %s accounts" % len(accounts))
|
||||
|
||||
# put a bigfile into each account
|
||||
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)
|
||||
log("created %s bigfiles" % len(accounts))
|
||||
|
||||
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
|
||||
chat = accounts[0].create_group_chat("stress-group")
|
||||
for addr in contact_addrs[1:]:
|
||||
chat.add_contact(chat.account.create_contact(addr))
|
||||
|
||||
# setup auto-responder bots which report back failures/actions
|
||||
report_queue = Queue()
|
||||
|
||||
def report_func(replier, report_type, *report_args):
|
||||
report_queue.put((replier, report_type, report_args))
|
||||
|
||||
# each replier receives all events and sends report events to receive_queue
|
||||
repliers = []
|
||||
for acc in accounts:
|
||||
replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func)
|
||||
acc.add_account_plugin(replier)
|
||||
repliers.append(replier)
|
||||
|
||||
# kick off message sending
|
||||
# after which repliers will reply to each other
|
||||
chat.send_text("hello")
|
||||
|
||||
alive_count = len(accounts)
|
||||
while alive_count > 0:
|
||||
try:
|
||||
replier, report_type, report_args = report_queue.get(timeout=10)
|
||||
except Empty:
|
||||
log("timeout waiting for next event")
|
||||
pytest.fail("timeout exceeded")
|
||||
if report_type == ReportType.exit:
|
||||
replier.log("EXIT")
|
||||
elif report_type == ReportType.ffi_error:
|
||||
replier.log("ERROR: {}".format(report_args[0]))
|
||||
elif report_type == ReportType.message_echo:
|
||||
continue
|
||||
else:
|
||||
raise ValueError("{} unknown report type {}, args={}".format(
|
||||
addr, report_type, report_args
|
||||
))
|
||||
alive_count -= 1
|
||||
replier.log("shutting down")
|
||||
replier.account.shutdown()
|
||||
replier.log("shut down complete, remaining={}".format(alive_count))
|
||||
|
||||
|
||||
class ReportType:
|
||||
exit = "exit"
|
||||
ffi_error = "ffi-error"
|
||||
message_echo = "message-echo"
|
||||
|
||||
|
||||
class AutoReplier:
|
||||
def __init__(self, account, log, num_send, num_bigfiles, report_func):
|
||||
self.account = account
|
||||
self._log = log
|
||||
self.report_func = report_func
|
||||
self.num_send = num_send
|
||||
self.num_bigfiles = num_bigfiles
|
||||
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.setDaemon(True)
|
||||
self._thread.start()
|
||||
|
||||
def log(self, message):
|
||||
self._log("{} {}".format(self.addr, message))
|
||||
|
||||
def thread_stats(self):
|
||||
# XXX later use, for now we just quit
|
||||
return
|
||||
while 1:
|
||||
time.sleep(1.0)
|
||||
break
|
||||
|
||||
@deltachat.account_hookimpl
|
||||
def ac_incoming_message(self, message):
|
||||
if self.current_sent >= self.num_send:
|
||||
self.report_func(self, ReportType.exit)
|
||||
return
|
||||
message.create_chat()
|
||||
message.mark_seen()
|
||||
self.log("incoming message: {}".format(message))
|
||||
|
||||
self.current_sent += 1
|
||||
# we are still alive, let's send a reply
|
||||
if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0:
|
||||
message.chat.send_text("send big file as reply to: {}".format(message.text))
|
||||
msg = message.chat.send_file(self.account.bigfile)
|
||||
else:
|
||||
msg = message.chat.send_text("got message id {}, small text reply".format(message.id))
|
||||
assert msg.text
|
||||
self.log("message-sent: {}".format(msg))
|
||||
self.report_func(self, ReportType.message_echo)
|
||||
if self.current_sent >= self.num_send:
|
||||
self.report_func(self, ReportType.exit)
|
||||
return
|
||||
|
||||
@deltachat.account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
self.log(ffi_event)
|
||||
if ffi_event.name == "DC_EVENT_ERROR":
|
||||
self.report_func(self, ReportType.ffi_error, ffi_event)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,37 +6,19 @@ import shutil
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
|
||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
||||
from deltachat import const
|
||||
|
||||
|
||||
def wait_msg_delivered(account, msg_list):
|
||||
""" 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")
|
||||
msg_list.remove((ev.data1, ev.data2))
|
||||
|
||||
|
||||
def wait_msgs_changed(account, msgs_list):
|
||||
""" 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:
|
||||
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
for i, (data1, data2) in enumerate(msgs_list):
|
||||
if ev.data1 == data1:
|
||||
if data2 is None or ev.data2 == data2:
|
||||
del msgs_list[i]
|
||||
break
|
||||
else:
|
||||
account.log("waiting mismatch data1={} data2={}".format(data1, data2))
|
||||
return ev.data1, ev.data2
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
|
||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||
chat = ac1.create_chat_by_contact(c2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
@@ -45,8 +27,13 @@ class TestOnlineInCreation:
|
||||
chat.prepare_message_file(src.strpath)
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
|
||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||
chat = ac1.create_chat_by_contact(c2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
@@ -58,10 +45,15 @@ class TestOnlineInCreation:
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
|
||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||
chat = ac1.create_chat_by_contact(c2)
|
||||
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||
wait_msgs_changed(ac1, 0, 0) # why no chat id?
|
||||
|
||||
lp.sec("create a message with a file in creation")
|
||||
orig = data.get_path("d.png")
|
||||
@@ -70,16 +62,19 @@ class TestOnlineInCreation:
|
||||
fp.write("preparing")
|
||||
prepared_original = chat.prepare_message_file(path)
|
||||
assert prepared_original.is_out_preparing()
|
||||
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
|
||||
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
||||
|
||||
lp.sec("forward the message while still in creation")
|
||||
chat2 = ac1.create_group_chat("newgroup")
|
||||
chat2.add_contact(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
|
||||
chat2.add_contact(c2)
|
||||
wait_msgs_changed(ac1, 0, 0) # why not chat id?
|
||||
ac1.forward_messages([prepared_original], chat2)
|
||||
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
|
||||
# is the one caused by forwarding
|
||||
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
forwarded_id = wait_msgs_changed(ac1, chat2.id)
|
||||
if forwarded_id == 0:
|
||||
forwarded_id = wait_msgs_changed(ac1, chat2.id)
|
||||
assert forwarded_id
|
||||
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||
assert forwarded_msg.is_out_preparing()
|
||||
|
||||
@@ -88,28 +83,30 @@ class TestOnlineInCreation:
|
||||
shutil.copyfile(orig, path)
|
||||
chat.send_prepared(prepared_original)
|
||||
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
|
||||
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
||||
|
||||
lp.sec("check that both forwarded and original message are proper.")
|
||||
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
|
||||
|
||||
lp.sec("expect the forwarded message to be sent now too")
|
||||
wait_msgs_changed(ac1, chat2.id, forwarded_id)
|
||||
fwd_msg = ac1.get_message_by_id(forwarded_id)
|
||||
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)
|
||||
])
|
||||
lp.sec("wait for the messages to be delivered to SMTP")
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev[1] == chat.id
|
||||
assert ev[2] == prepared_original.id
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
assert ev[1] == chat2.id
|
||||
assert ev[2] == forwarded_id
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
received_original = ac2.get_message_by_id(ev1.data2)
|
||||
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
received_original = ac2.get_message_by_id(ev1[2])
|
||||
assert cmp(received_original.filename, orig, shallow=False)
|
||||
|
||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev2.data1 != ev1.data1
|
||||
received_copy = ac2.get_message_by_id(ev2.data2)
|
||||
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev2[1] != ev1[1]
|
||||
received_copy = ac2.get_message_by_id(ev2[2])
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
|
||||
@@ -1,52 +1,63 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from queue import Queue
|
||||
from deltachat import capi, cutil, const
|
||||
from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
capi.lib.dc_close(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
def test_callback_None2int():
|
||||
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
|
||||
set_context_callback(ctx, lambda *args: None)
|
||||
capi.lib.dc_close(ctx)
|
||||
clear_context_callback(ctx)
|
||||
|
||||
# register after_shutdown function
|
||||
shutdowns = Queue()
|
||||
|
||||
class ShutdownPlugin:
|
||||
@global_hookimpl
|
||||
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")
|
||||
def test_dc_close_events(tmpdir):
|
||||
from deltachat.account import Account
|
||||
p = tmpdir.join("hello.db")
|
||||
ac1 = Account(p.strpath)
|
||||
ac1.shutdown()
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
def find(info_string):
|
||||
evlog = ac1._evlogger
|
||||
while 1:
|
||||
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
||||
data2 = ev[2]
|
||||
if info_string in data2:
|
||||
return
|
||||
else:
|
||||
print("skipping event", *ev)
|
||||
|
||||
find("disconnecting inbox-thread")
|
||||
find("disconnecting sentbox-thread")
|
||||
find("disconnecting mvbox-thread")
|
||||
find("disconnecting SMTP")
|
||||
find("Database closed")
|
||||
|
||||
|
||||
def test_wrong_db(tmpdir):
|
||||
dc_context = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
p = tmpdir.join("hello.db")
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
|
||||
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
# Apparently some client code expects this to be the same as passing NULL.
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert ctx != ffi.NULL
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
|
||||
|
||||
|
||||
def test_event_defines():
|
||||
@@ -55,27 +66,24 @@ def test_event_defines():
|
||||
|
||||
|
||||
def test_sig():
|
||||
sig = capi.lib.dc_event_has_string_data
|
||||
assert not sig(const.DC_EVENT_MSGS_CHANGED)
|
||||
assert sig(const.DC_EVENT_INFO)
|
||||
assert sig(const.DC_EVENT_WARNING)
|
||||
assert sig(const.DC_EVENT_ERROR)
|
||||
assert sig(const.DC_EVENT_SMTP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_IMAP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||
sig = capi.lib.dc_get_event_signature_types
|
||||
assert sig(const.DC_EVENT_INFO) == 2
|
||||
assert sig(const.DC_EVENT_WARNING) == 2
|
||||
assert sig(const.DC_EVENT_ERROR) == 2
|
||||
assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2
|
||||
assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2
|
||||
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = contact1.create_chat()
|
||||
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
|
||||
chat = ac1.create_chat_by_contact(contact1)
|
||||
chat.send_text("one messae")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg_ids = [9]
|
||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
|
||||
|
||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
@@ -87,18 +95,47 @@ def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
|
||||
def test_provider_info_none():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_open(tmpdir):
|
||||
db_fname = tmpdir.join("test.db")
|
||||
def test_get_info_closed():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' not in info
|
||||
|
||||
|
||||
def test_get_info_open(tmpdir):
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
db_fname = tmpdir.join("test.db")
|
||||
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' in info
|
||||
|
||||
|
||||
def test_is_open_closed():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert lib.dc_is_open(ctx) == 0
|
||||
|
||||
|
||||
def test_is_open_actually_open(tmpdir):
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
db_fname = tmpdir.join("test.db")
|
||||
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
|
||||
assert lib.dc_is_open(ctx) == 1
|
||||
|
||||
@@ -7,14 +7,13 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
deps =
|
||||
@@ -41,13 +40,13 @@ deps =
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
flake8 src/deltachat
|
||||
flake8 tests/ examples/
|
||||
flake8 tests/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx
|
||||
sphinx==2.2.0
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
@@ -67,12 +66,11 @@ commands =
|
||||
|
||||
[pytest]
|
||||
addopts = -v -ra --strict-markers
|
||||
python_files = tests/test_*.py
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 90
|
||||
timeout_method = thread
|
||||
markers =
|
||||
ignored: ignore this test in default test runs, use --ignored to run.
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.43.1
|
||||
nightly-2019-11-06
|
||||
|
||||
21
spec.md
21
spec.md
@@ -1,12 +1,10 @@
|
||||
# chat-mail specification
|
||||
# Chat-over-Email specification
|
||||
|
||||
Version: 0.32.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
Version 0.30.0
|
||||
|
||||
This document roughly describes how chat-mail
|
||||
apps use the standard e-mail system
|
||||
to implement typical messenger functions.
|
||||
This document describes how emails can be used
|
||||
to implement typical messenger functions
|
||||
while staying compatible to existing MUAs.
|
||||
|
||||
- [Encryption](#encryption)
|
||||
- [Outgoing messages](#outgoing-messages)
|
||||
@@ -32,14 +30,17 @@ Messages SHOULD be encrypted by the
|
||||
`prefer-encrypt=mutual` MAY be set by default.
|
||||
|
||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
|
||||
If Memoryhole is not used,
|
||||
the subject of encrypted messages SHOULD be replaced by the string `...`.
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
|
||||
For filtering and smart appearance of the messages in normal MUAs,
|
||||
the `Subject` header SHOULD be `Message from <sender name>`.
|
||||
the `Subject` header SHOULD start with the characters `Chat:`
|
||||
and SHOULD be an excerpt of the message.
|
||||
Replies to messages MAY follow the typical `Re:`-format.
|
||||
|
||||
The body MAY contain text which MUST have the content type `text/plain`
|
||||
@@ -57,7 +58,7 @@ Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Subject: Message from sender@domain
|
||||
Subject: Chat: Hello ...
|
||||
|
||||
Hello world!
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Aheader {
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Self::from_str(&value) {
|
||||
Ok(header) => {
|
||||
if addr_cmp(&header.addr, wanted_from) {
|
||||
@@ -127,10 +127,14 @@ impl str::FromStr for Aheader {
|
||||
.split(';')
|
||||
.filter_map(|a| {
|
||||
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
||||
match &attribute[..] {
|
||||
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
|
||||
_ => None,
|
||||
if attribute.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
attribute[0].trim().to_string(),
|
||||
attribute[1].trim().to_string(),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
352
src/blob.rs
352
src/blob.rs
@@ -2,20 +2,16 @@
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use image::GenericImageView;
|
||||
use num_traits::FromPrimitive;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use self::image::GenericImageView;
|
||||
use crate::constants::AVATAR_SIZE;
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::message;
|
||||
|
||||
extern crate image;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
///
|
||||
@@ -47,35 +43,31 @@ impl<'a> BlobObject<'a> {
|
||||
/// [BlobError::WriteFailure] is used when the file could not
|
||||
/// be written to. You can expect [BlobError.cause] to contain an
|
||||
/// underlying error.
|
||||
pub async fn create(
|
||||
pub fn create(
|
||||
context: &'a Context,
|
||||
suggested_name: impl AsRef<str>,
|
||||
data: &[u8],
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let blobdir = context.get_blobdir();
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
|
||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
|
||||
file.write_all(data)
|
||||
.await
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: blobdir.to_path_buf(),
|
||||
blobname: name.clone(),
|
||||
cause: err.into(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
// Creates a new file, returning a tuple of the name and the handle.
|
||||
async fn create_new_file(
|
||||
dir: &Path,
|
||||
stem: &str,
|
||||
ext: &str,
|
||||
) -> Result<(String, fs::File), BlobError> {
|
||||
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
|
||||
let max_attempt = 15;
|
||||
let mut name = format!("{}{}", stem, ext);
|
||||
for attempt in 0..max_attempt {
|
||||
@@ -84,7 +76,6 @@ impl<'a> BlobObject<'a> {
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.await
|
||||
{
|
||||
Ok(file) => return Ok((name, file)),
|
||||
Err(err) => {
|
||||
@@ -93,6 +84,7 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
@@ -105,6 +97,7 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,41 +113,39 @@ impl<'a> BlobObject<'a> {
|
||||
/// In addition to the errors in [BlobObject::create] the
|
||||
/// [BlobError::CopyFailure] is used when the data can not be
|
||||
/// copied.
|
||||
pub async fn create_and_copy(
|
||||
pub fn create_and_copy(
|
||||
context: &'a Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let mut src_file =
|
||||
fs::File::open(src.as_ref())
|
||||
.await
|
||||
.map_err(|err| BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: String::from(""),
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
})?;
|
||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: String::from(""),
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||
let (name, mut dst_file) =
|
||||
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
||||
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
|
||||
let name_for_err = name.clone();
|
||||
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
|
||||
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
let path = context.get_blobdir().join(&name_for_err);
|
||||
fs::remove_file(path).await.ok();
|
||||
fs::remove_file(path).ok();
|
||||
}
|
||||
return Err(BlobError::CopyFailure {
|
||||
BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: name_for_err,
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
@@ -172,14 +163,14 @@ impl<'a> BlobObject<'a> {
|
||||
/// This merely delegates to the [BlobObject::create_and_copy] and
|
||||
/// the [BlobObject::from_path] methods. See those for possible
|
||||
/// errors.
|
||||
pub async fn new_from_path(
|
||||
pub fn new_from_path(
|
||||
context: &Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
||||
) -> std::result::Result<BlobObject, BlobError> {
|
||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
||||
BlobObject::from_path(context, src)
|
||||
} else {
|
||||
BlobObject::create_and_copy(context, src).await
|
||||
BlobObject::create_and_copy(context, src)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,14 +198,17 @@ impl<'a> BlobObject<'a> {
|
||||
.map_err(|_| BlobError::WrongBlobdir {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
src: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
}
|
||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
}
|
||||
@@ -242,6 +236,7 @@ impl<'a> BlobObject<'a> {
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: PathBuf::from(name),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
}
|
||||
Ok(BlobObject {
|
||||
@@ -364,6 +359,7 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
|
||||
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
|
||||
@@ -373,47 +369,10 @@ impl<'a> BlobObject<'a> {
|
||||
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
|
||||
|
||||
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err.into(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
|
||||
!= Some((Viewtype::Image, "image/jpeg"))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err,
|
||||
})?;
|
||||
|
||||
let img_wh = if MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
|
||||
.unwrap_or_default()
|
||||
== MediaQuality::Balanced
|
||||
{
|
||||
BALANCED_IMAGE_SIZE
|
||||
} else {
|
||||
WORSE_IMAGE_SIZE
|
||||
};
|
||||
|
||||
if img.width() <= img_wh && img.height() <= img_wh {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||
cause: err.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -427,41 +386,98 @@ impl<'a> fmt::Display for BlobObject<'a> {
|
||||
}
|
||||
|
||||
/// Errors for the [BlobObject].
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum BlobError {
|
||||
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
|
||||
CreateFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[source]
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
|
||||
WriteFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
|
||||
CopyFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
src: PathBuf,
|
||||
#[source]
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
|
||||
RecodeFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[source]
|
||||
#[cause]
|
||||
cause: image::ImageError,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
|
||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||
#[error("Blob has a badname {}", .blobname.display())]
|
||||
WrongName { blobname: PathBuf },
|
||||
WrongBlobdir {
|
||||
blobdir: PathBuf,
|
||||
src: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
WrongName {
|
||||
blobname: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
}
|
||||
|
||||
// Implementing Display is done by hand because the failure
|
||||
// #[fail(display = "...")] syntax does not allow using
|
||||
// `blobdir.display()`.
|
||||
impl fmt::Display for BlobError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Match on the data rather than kind, they are equivalent for
|
||||
// identifying purposes but contain the actual data we need.
|
||||
match &self {
|
||||
BlobError::CreateFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to create blob {} in {}",
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::WriteFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to write data to blob {} in {}",
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::CopyFailure {
|
||||
blobdir,
|
||||
blobname,
|
||||
src,
|
||||
..
|
||||
} => write!(
|
||||
f,
|
||||
"Failed to copy data from {} to blob {} in {}",
|
||||
src.display(),
|
||||
blobname,
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::RecodeFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
|
||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
||||
f,
|
||||
"File path {} is not in blobdir {}",
|
||||
src.display(),
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::WrongName { blobname, .. } => {
|
||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -470,71 +486,58 @@ mod tests {
|
||||
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
|
||||
#[test]
|
||||
fn test_create() {
|
||||
let t = dummy_context();
|
||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap();
|
||||
let fname = t.ctx.get_blobdir().join("foo");
|
||||
let data = fs::read(fname).await.unwrap();
|
||||
let data = fs::read(fname).unwrap();
|
||||
assert_eq!(data, b"hello");
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
|
||||
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_lowercase_ext() {
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_lowercase_ext() {
|
||||
let t = dummy_context();
|
||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_file_name() {
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_as_file_name() {
|
||||
let t = dummy_context();
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
||||
assert_eq!(blob.as_file_name(), "foo.txt");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_rel_path() {
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_as_rel_path() {
|
||||
let t = dummy_context();
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
||||
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_suffix() {
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_suffix() {
|
||||
let t = dummy_context();
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
||||
assert_eq!(blob.suffix(), Some("txt"));
|
||||
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
|
||||
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap();
|
||||
assert_eq!(blob.suffix(), None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_dup() {
|
||||
let t = dummy_context().await;
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_create_dup() {
|
||||
let t = dummy_context();
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
||||
let foo_path = t.ctx.get_blobdir().join("foo.txt");
|
||||
assert!(foo_path.exists().await);
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"world")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||
while let Some(dirent) = dir.next().await {
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap();
|
||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
||||
let fname = dirent.unwrap().file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
|
||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
assert!(name.starts_with("foo"));
|
||||
@@ -543,22 +546,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_double_ext_preserved() {
|
||||
let t = dummy_context().await;
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_double_ext_preserved() {
|
||||
let t = dummy_context();
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap();
|
||||
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
|
||||
assert!(foo_path.exists().await);
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
|
||||
while let Some(dirent) = dir.next().await {
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap();
|
||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
||||
let fname = dirent.unwrap().file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
|
||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
println!("{}", name);
|
||||
@@ -568,55 +566,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_long_names() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_long_names() {
|
||||
let t = dummy_context();
|
||||
let s = "1".repeat(150);
|
||||
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
|
||||
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
assert!(blobname.len() < 128);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_and_copy() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_and_copy() {
|
||||
let t = dummy_context();
|
||||
let src = t.dir.path().join("src");
|
||||
fs::write(&src, b"boo").await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
|
||||
fs::write(&src, b"boo").unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let whoops = t.dir.path().join("whoops");
|
||||
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
|
||||
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err());
|
||||
let whoops = t.ctx.get_blobdir().join("whoops");
|
||||
assert!(!whoops.exists().await);
|
||||
assert!(!whoops.exists());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_from_path() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_from_path() {
|
||||
let t = dummy_context();
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
|
||||
fs::write(&src_ext, b"boo").unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let src_int = t.ctx.get_blobdir().join("internal");
|
||||
fs::write(&src_int, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
|
||||
fs::write(&src_int, b"boo").unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
}
|
||||
#[async_std::test]
|
||||
async fn test_create_from_name_long() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_from_name_long() {
|
||||
let t = dummy_context();
|
||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
|
||||
fs::write(&src_ext, b"boo").unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
|
||||
assert_eq!(
|
||||
blob.as_name(),
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
|
||||
2291
src/chat.rs
2291
src/chat.rs
File diff suppressed because it is too large
Load Diff
296
src/chatlist.rs
296
src/chatlist.rs
@@ -1,11 +1,10 @@
|
||||
//! # Chat list module
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::error::Result;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::stock::StockMessage;
|
||||
@@ -74,9 +73,6 @@ impl Chatlist {
|
||||
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -86,23 +82,12 @@ impl Chatlist {
|
||||
/// are returned.
|
||||
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
|
||||
/// are returned.
|
||||
pub async fn try_load(
|
||||
pub fn try_load(
|
||||
context: &Context,
|
||||
listflags: usize,
|
||||
query: Option<&str>,
|
||||
query_contact_id: Option<u32>,
|
||||
) -> Result<Self> {
|
||||
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
|
||||
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
|
||||
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
|
||||
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
|
||||
|
||||
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||
// messages get deleted to avoid reloading the same chatlist.
|
||||
if let Err(err) = delete_device_expired_messages(context).await {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
@@ -116,15 +101,6 @@ impl Chatlist {
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
@@ -157,19 +133,14 @@ impl Chatlist {
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
)?
|
||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
||||
// show archived chats
|
||||
// (this includes the archived device-chat; we could skip it,
|
||||
// however, then the number of archived chats do not match, which might be even more irritating.
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
@@ -183,26 +154,23 @@ impl Chatlist {
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
params![MessageState::OutDraft],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else if let Some(query) = query {
|
||||
let query = query.trim().to_string();
|
||||
ensure!(!query.is_empty(), "missing query");
|
||||
|
||||
// allow searching over special names that may change at any time
|
||||
// when the ui calls set_stock_translation()
|
||||
if let Err(err) = update_special_chat_names(context).await {
|
||||
if let Err(err) = update_special_chat_names(context) {
|
||||
warn!(context, "cannot update special chat names: {:?}", err)
|
||||
}
|
||||
|
||||
let str_like_cmd = format!("%{}%", query);
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
@@ -210,27 +178,18 @@ impl Chatlist {
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.name LIKE ?3
|
||||
AND c.name LIKE ?
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
params![MessageState::OutDraft, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
@@ -241,32 +200,29 @@ impl Chatlist {
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?3
|
||||
AND NOT c.archived=?2
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||
{
|
||||
if !flag_for_forwarding {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
}
|
||||
)?;
|
||||
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
|
||||
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||
}
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||
@@ -289,20 +245,18 @@ impl Chatlist {
|
||||
///
|
||||
/// To get the message object from the message ID, use dc_get_chat().
|
||||
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
||||
match self.ids.get(index) {
|
||||
Some((chat_id, _msg_id)) => *chat_id,
|
||||
None => ChatId::new(0),
|
||||
if index >= self.ids.len() {
|
||||
return ChatId::new(0);
|
||||
}
|
||||
self.ids[index].0
|
||||
}
|
||||
|
||||
/// Get a single message ID of a chatlist.
|
||||
///
|
||||
/// To get the message object from the message ID, use dc_get_msg().
|
||||
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
||||
match self.ids.get(index) {
|
||||
Some((_chat_id, msg_id)) => Ok(*msg_id),
|
||||
None => bail!("Chatlist index out of range"),
|
||||
}
|
||||
ensure!(index < self.ids.len(), "Chatlist index out of range");
|
||||
Ok(self.ids[index].1)
|
||||
}
|
||||
|
||||
/// Get a summary for a chatlist index.
|
||||
@@ -319,38 +273,36 @@ impl Chatlist {
|
||||
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
|
||||
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
|
||||
// 0 if not applicable.
|
||||
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
|
||||
pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
|
||||
// The summary is created by the chat, not by the last message.
|
||||
// This is because we may want to display drafts here or stuff as
|
||||
// "is typing".
|
||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||
let mut ret = Lot::new();
|
||||
|
||||
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||
Some(ids) => ids,
|
||||
None => {
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if index >= self.ids.len() {
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return ret;
|
||||
}
|
||||
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
|
||||
chat_loaded = chat;
|
||||
&chat_loaded
|
||||
} else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let lastmsg_id = self.ids[index].1;
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
|
||||
}
|
||||
|
||||
Some(lastmsg)
|
||||
@@ -362,15 +314,9 @@ impl Chatlist {
|
||||
ret.text2 = None;
|
||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
||||
{
|
||||
ret.text2 = Some(
|
||||
context
|
||||
.stock_str(StockMessage::NoMessages)
|
||||
.await
|
||||
.to_string(),
|
||||
);
|
||||
ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string());
|
||||
} else {
|
||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
|
||||
.await;
|
||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context);
|
||||
}
|
||||
|
||||
ret
|
||||
@@ -382,38 +328,34 @@ impl Chatlist {
|
||||
}
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsv![],
|
||||
params![],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
// We have an index over the state-column, this should be
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
context.sql.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
params![],
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -422,21 +364,15 @@ mod tests {
|
||||
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_try_load() {
|
||||
let t = dummy_context().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_try_load() {
|
||||
let t = dummy_context();
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
@@ -445,102 +381,44 @@ mod tests {
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
chat_id2.set_draft(&t.ctx, Some(&mut msg));
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
// check chatlist query and archive functionality
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.await
|
||||
.ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = dummy_context().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
#[test]
|
||||
fn test_search_special_chat_names() {
|
||||
let t = dummy_context();
|
||||
t.ctx.update_device_chats().unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = dummy_context().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = dummy_context().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
chat_id1.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t.ctx, 0, None).await;
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
183
src/config.rs
183
src/config.rs
@@ -4,14 +4,13 @@ use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::events::Event;
|
||||
use crate::message::MsgId;
|
||||
use crate::job::*;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::{scheduler::InterruptInfo, stock::StockMessage};
|
||||
use crate::stock::StockMessage;
|
||||
use rusqlite::NO_PARAMS;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -63,31 +62,9 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
///
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// device.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
SaveMimeHeaders,
|
||||
ConfiguredAddr,
|
||||
ConfiguredMailServer,
|
||||
@@ -104,9 +81,6 @@ pub enum Config {
|
||||
ConfiguredServerFlags,
|
||||
ConfiguredSendSecurity,
|
||||
ConfiguredE2EEEnabled,
|
||||
ConfiguredInboxFolder,
|
||||
ConfiguredMvboxFolder,
|
||||
ConfiguredSentboxFolder,
|
||||
Configured,
|
||||
|
||||
#[strum(serialize = "sys.version")]
|
||||
@@ -121,16 +95,16 @@ pub enum Config {
|
||||
|
||||
impl Context {
|
||||
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||
pub async fn get_config(&self, key: Config) -> Option<String> {
|
||||
pub fn get_config(&self, key: Config) -> Option<String> {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(self, key).await;
|
||||
let rel_path = self.sql.get_raw_config(self, key);
|
||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(self, key).await,
|
||||
_ => self.sql.get_raw_config(self, key),
|
||||
};
|
||||
|
||||
if value.is_some() {
|
||||
@@ -139,104 +113,65 @@ impl Context {
|
||||
|
||||
// Default values
|
||||
match key {
|
||||
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
|
||||
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).into_owned()),
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_config_int(&self, key: Config) -> i32 {
|
||||
pub fn get_config_int(&self, key: Config) -> i32 {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn get_config_bool(&self, key: Config) -> bool {
|
||||
self.get_config_int(key).await != 0
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await {
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x as i64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
/// after `x` seconds.
|
||||
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await {
|
||||
0 => None,
|
||||
x => Some(x as i64),
|
||||
}
|
||||
pub fn get_config_bool(&self, key: Config) -> bool {
|
||||
self.get_config_int(key) != 0
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.await?;
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||
.await?;
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let blob = BlobObject::new_from_path(&self, value).await?;
|
||||
let blob = BlobObject::new_from_path(&self, value)?;
|
||||
blob.recode_to_avatar_size(self)?;
|
||||
self.sql
|
||||
.set_raw_config(self, key, Some(blob.as_name()))
|
||||
.await
|
||||
self.sql.set_raw_config(self, key, Some(blob.as_name()))
|
||||
}
|
||||
None => self.sql.set_raw_config(self, key, None).await,
|
||||
None => self.sql.set_raw_config(self, key, None),
|
||||
}
|
||||
}
|
||||
Config::InboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_inbox(InterruptInfo::new(false, None)).await;
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
interrupt_inbox_idle(self);
|
||||
ret
|
||||
}
|
||||
Config::SentboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_sentbox(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
interrupt_sentbox_idle(self);
|
||||
ret
|
||||
}
|
||||
Config::MvboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_mvbox(InterruptInfo::new(false, None)).await;
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
interrupt_mvbox_idle(self);
|
||||
ret
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = self.stock_str(StockMessage::StatusLine).await;
|
||||
let def = self.stock_str(StockMessage::StatusLine);
|
||||
let val = if value.is_none() || value.unwrap() == def {
|
||||
None
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
self.sql.set_raw_config(self, key, val).await
|
||||
self.sql.set_raw_config(self, key, val)
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.emit_event(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value).await,
|
||||
_ => self.sql.set_raw_config(self, key, value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,11 +194,9 @@ mod tests {
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::constants::AVATAR_SIZE;
|
||||
use crate::test_utils::*;
|
||||
use image::GenericImageView;
|
||||
use num_traits::FromPrimitive;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
@@ -284,9 +217,9 @@ mod tests {
|
||||
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_selfavatar_outside_blobdir() {
|
||||
let t = dummy_context();
|
||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
File::create(&avatar_src)
|
||||
@@ -294,14 +227,13 @@ mod tests {
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
assert!(!avatar_blob.exists());
|
||||
t.ctx
|
||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists().await);
|
||||
assert!(avatar_blob.exists());
|
||||
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
|
||||
let img = image::open(avatar_src).unwrap();
|
||||
@@ -313,9 +245,9 @@ mod tests {
|
||||
assert_eq!(img.height(), AVATAR_SIZE);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_selfavatar_in_blobdir() {
|
||||
let t = dummy_context();
|
||||
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
@@ -329,57 +261,12 @@ mod tests {
|
||||
|
||||
t.ctx
|
||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
||||
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
|
||||
|
||||
let img = image::open(avatar_src).unwrap();
|
||||
assert_eq!(img.width(), AVATAR_SIZE);
|
||||
assert_eq!(img.height(), AVATAR_SIZE);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = dummy_context().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
File::create(&avatar_src)
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.unwrap();
|
||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
|
||||
assert!(!avatar_blob.exists().await);
|
||||
t.ctx
|
||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists().await);
|
||||
assert_eq!(
|
||||
std::fs::metadata(&avatar_blob).unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_media_quality_config_option() {
|
||||
let t = dummy_context().await;
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||
assert_eq!(media_quality, 0);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Balanced);
|
||||
|
||||
t.ctx
|
||||
.set_config(Config::MediaQuality, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||
assert_eq!(media_quality, 1);
|
||||
assert_eq!(constants::MediaQuality::Worse as i32, 1);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Worse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
use crate::constants::*;
|
||||
@@ -8,7 +9,33 @@ use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Invalid email address: {:?}", _0)]
|
||||
InvalidEmailAddress(String),
|
||||
|
||||
#[fail(display = "XML error at position {}", position)]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[cause]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[fail(display = "Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[fail(display = "Failed to get URL {}", _0)]
|
||||
ReadUrlError(#[cause] super::read_url::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<super::read_url::Error> for Error {
|
||||
fn from(err: super::read_url::Error) -> Error {
|
||||
Error::ReadUrlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure<'a> {
|
||||
@@ -38,16 +65,16 @@ enum MozConfigTag {
|
||||
Username,
|
||||
}
|
||||
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
// Split address into local part and domain part.
|
||||
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||
[domain, local] => (local, domain),
|
||||
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||
};
|
||||
let p = in_emailaddr
|
||||
.find('@')
|
||||
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
|
||||
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
|
||||
let in_emaildomain = &in_emaildomain[1..];
|
||||
|
||||
let mut moz_ac = MozAutoconfigure {
|
||||
in_emailaddr,
|
||||
@@ -94,12 +121,12 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn moz_autoconfigure(
|
||||
pub fn moz_autoconfigure(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
let xml_raw = read_url(context, url).await?;
|
||||
) -> Result<LoginParam> {
|
||||
let xml_raw = read_url(context, url)?;
|
||||
|
||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||
if let Err(err) = &res {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Outlook's Autodiscover
|
||||
|
||||
use quick_xml;
|
||||
use quick_xml::events::BytesEnd;
|
||||
|
||||
use crate::constants::*;
|
||||
@@ -7,7 +8,33 @@ use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "XML error at position {}", position)]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
#[cause]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[fail(display = "Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[fail(display = "Failed to get URL {}", _0)]
|
||||
ReadUrlError(#[cause] super::read_url::Error),
|
||||
|
||||
#[fail(display = "Number of redirection is exceeded")]
|
||||
RedirectionError,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<super::read_url::Error> for Error {
|
||||
fn from(err: super::read_url::Error) -> Error {
|
||||
Error::ReadUrlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
struct OutlookAutodiscover {
|
||||
pub out: LoginParam,
|
||||
@@ -25,7 +52,7 @@ enum ParsingResult {
|
||||
RedirectUrl(String),
|
||||
}
|
||||
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
||||
let mut outlk_ad = OutlookAutodiscover {
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
@@ -112,15 +139,15 @@ fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn outlk_autodiscover(
|
||||
pub fn outlk_autodiscover(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
_param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
) -> Result<LoginParam> {
|
||||
let mut url = url.to_string();
|
||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||
for _i in 0..10 {
|
||||
let xml_raw = read_url(context, &url).await?;
|
||||
let xml_raw = read_url(context, &url)?;
|
||||
let res = parse_xml(&xml_raw);
|
||||
if let Err(err) = &res {
|
||||
warn!(context, "{}", err);
|
||||
|
||||
1000
src/configure/mod.rs
1000
src/configure/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,21 @@
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[error("URL request error")]
|
||||
GetError(surf::Error),
|
||||
#[fail(display = "URL request error")]
|
||||
GetError(#[cause] reqwest::Error),
|
||||
}
|
||||
|
||||
pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn read_url(context: &Context, url: &str) -> Result<String> {
|
||||
info!(context, "Requesting URL {}", url);
|
||||
|
||||
match surf::get(url).recv_string().await {
|
||||
match reqwest::blocking::Client::new()
|
||||
.get(url)
|
||||
.send()
|
||||
.and_then(|res| res.text())
|
||||
{
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
info!(context, "Can\'t read URL {}", url);
|
||||
|
||||
@@ -57,19 +57,6 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
impl Default for MediaQuality {
|
||||
fn default() -> Self {
|
||||
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
@@ -93,7 +80,6 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
|
||||
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
||||
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
|
||||
|
||||
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
|
||||
@@ -210,13 +196,13 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
||||
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
|
||||
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
|
||||
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
||||
|
||||
// QR code scanning (view from Bob, the joiner)
|
||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||
@@ -227,13 +213,6 @@ pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
|
||||
// max. width/height of images
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
pub const WORSE_IMAGE_SIZE: u32 = 640;
|
||||
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
@@ -331,6 +310,8 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
||||
const DC_STR_NOMESSAGES: usize = 1;
|
||||
const DC_STR_SELF: usize = 2;
|
||||
const DC_STR_DRAFT: usize = 3;
|
||||
const DC_STR_MEMBER: usize = 4;
|
||||
const DC_STR_CONTACT: usize = 6;
|
||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||
const DC_STR_DEADDROP: usize = 8;
|
||||
const DC_STR_IMAGE: usize = 9;
|
||||
@@ -362,6 +343,7 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
|
||||
const DC_STR_STARREDMSGS: usize = 41;
|
||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||
|
||||
703
src/contact.rs
703
src/contact.rs
File diff suppressed because it is too large
Load Diff
487
src/context.rs
487
src/context.rs
@@ -1,70 +1,68 @@
|
||||
//! Context module
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::events::{Event, EventEmitter, Events};
|
||||
use crate::job::{self, Action};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
use crate::job::*;
|
||||
use crate::job_thread::JobThread;
|
||||
use crate::key::Key;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Callback function type for [Context]
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `context` - The context object as returned by [Context::new].
|
||||
/// * `event` - One of the [Event] items.
|
||||
/// * `data1` - Depends on the event parameter, see [Event].
|
||||
/// * `data2` - Depends on the event parameter, see [Event].
|
||||
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
|
||||
|
||||
#[derive(DebugStub)]
|
||||
pub struct Context {
|
||||
pub(crate) inner: Arc<InnerContext>,
|
||||
}
|
||||
|
||||
impl Deref for Context {
|
||||
type Target = InnerContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InnerContext {
|
||||
/// Database file path
|
||||
pub(crate) dbfile: PathBuf,
|
||||
dbfile: PathBuf,
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) os_name: Option<String>,
|
||||
pub(crate) bob: RwLock<BobStatus>,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
blobdir: PathBuf,
|
||||
pub sql: Sql,
|
||||
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
|
||||
pub probe_imap_network: Arc<RwLock<bool>>,
|
||||
pub inbox_thread: Arc<RwLock<JobThread>>,
|
||||
pub sentbox_thread: Arc<RwLock<JobThread>>,
|
||||
pub mvbox_thread: Arc<RwLock<JobThread>>,
|
||||
pub smtp: Arc<Mutex<Smtp>>,
|
||||
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
|
||||
pub oauth2_critical: Arc<Mutex<()>>,
|
||||
#[debug_stub = "Callback"]
|
||||
cb: Box<ContextCallback>,
|
||||
pub os_name: Option<String>,
|
||||
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
|
||||
pub(crate) bob: Arc<RwLock<BobStatus>>,
|
||||
pub last_smeared_timestamp: RwLock<i64>,
|
||||
pub running_state: Arc<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.
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
|
||||
creation_time: SystemTime,
|
||||
pub generating_key_mutex: Mutex<()>,
|
||||
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct RunningState {
|
||||
pub ongoing_running: bool,
|
||||
shall_stop_ongoing: bool,
|
||||
cancel_sender: Option<Sender<()>>,
|
||||
}
|
||||
|
||||
/// Return some info about deltachat-core
|
||||
@@ -73,8 +71,8 @@ pub struct RunningState {
|
||||
/// actual keys and their values which will be present are not
|
||||
/// guaranteed. Calling [Context::get_info] also includes information
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
pub fn get_info() -> HashMap<&'static str, String> {
|
||||
let mut res = HashMap::new();
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -84,95 +82,72 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
let blobdir = dbfile.with_file_name(blob_fname);
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
if !blobdir.exists() {
|
||||
std::fs::create_dir_all(&blobdir)?;
|
||||
}
|
||||
Context::with_blobdir(os_name, dbfile, blobdir).await
|
||||
Context::with_blobdir(cb, os_name, dbfile, blobdir)
|
||||
}
|
||||
|
||||
pub async fn with_blobdir(
|
||||
pub fn with_blobdir(
|
||||
cb: Box<ContextCallback>,
|
||||
os_name: String,
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
) -> Result<Context> {
|
||||
ensure!(
|
||||
blobdir.is_dir().await,
|
||||
blobdir.is_dir(),
|
||||
"Blobdir does not exist: {}",
|
||||
blobdir.display()
|
||||
);
|
||||
|
||||
let inner = InnerContext {
|
||||
let ctx = Context {
|
||||
blobdir,
|
||||
dbfile,
|
||||
cb,
|
||||
os_name: Some(os_name),
|
||||
running_state: RwLock::new(Default::default()),
|
||||
running_state: Arc::new(RwLock::new(Default::default())),
|
||||
sql: Sql::new(),
|
||||
bob: RwLock::new(Default::default()),
|
||||
smtp: Arc::new(Mutex::new(Smtp::new())),
|
||||
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
|
||||
oauth2_critical: Arc::new(Mutex::new(())),
|
||||
bob: Arc::new(RwLock::new(Default::default())),
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
|
||||
inbox_thread: Arc::new(RwLock::new(JobThread::new(
|
||||
"INBOX",
|
||||
"configured_inbox_folder",
|
||||
Imap::new(),
|
||||
))),
|
||||
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
|
||||
"SENTBOX",
|
||||
"configured_sentbox_folder",
|
||||
Imap::new(),
|
||||
))),
|
||||
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
|
||||
"MVBOX",
|
||||
"configured_mvbox_folder",
|
||||
Imap::new(),
|
||||
))),
|
||||
probe_imap_network: Arc::new(RwLock::new(false)),
|
||||
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events: Events::default(),
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
ensure!(
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false),
|
||||
"Failed opening sqlite database"
|
||||
);
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
/// Starts the IO scheduler.
|
||||
pub async fn start_io(&self) {
|
||||
info!(self, "starting IO");
|
||||
if self.is_io_running().await {
|
||||
info!(self, "IO is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
let l = &mut *self.inner.scheduler.write().await;
|
||||
l.start(self.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the IO scheduler is running.
|
||||
pub async fn is_io_running(&self) -> bool {
|
||||
self.inner.is_io_running().await
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
pub async fn stop_io(&self) {
|
||||
info!(self, "stopping IO");
|
||||
if !self.is_io_running().await {
|
||||
info!(self, "IO is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner.stop_io().await;
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
///
|
||||
/// Warning: this is only here for testing, not part of the public API.
|
||||
#[cfg(feature = "internals")]
|
||||
pub fn sql(&self) -> &Sql {
|
||||
&self.inner.sql
|
||||
}
|
||||
|
||||
/// Returns database file path.
|
||||
pub fn get_dbfile(&self) -> &Path {
|
||||
self.dbfile.as_path()
|
||||
@@ -183,57 +158,49 @@ impl Context {
|
||||
self.blobdir.as_path()
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: Event) {
|
||||
self.events.emit(event);
|
||||
pub fn call_cb(&self, event: Event) {
|
||||
(*self.cb)(self, event);
|
||||
}
|
||||
|
||||
/// Get the next queued event.
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
/*******************************************************************************
|
||||
* Ongoing process allocation/free/check
|
||||
******************************************************************************/
|
||||
|
||||
// Ongoing process allocation/free/check
|
||||
pub fn alloc_ongoing(&self) -> bool {
|
||||
if self.has_ongoing() {
|
||||
warn!(self, "There is already another ongoing process running.",);
|
||||
|
||||
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
if self.has_ongoing().await {
|
||||
bail!("There is already another ongoing process running.");
|
||||
false
|
||||
} else {
|
||||
let s_a = self.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
let (sender, receiver) = channel(1);
|
||||
s.cancel_sender = Some(sender);
|
||||
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub async fn free_ongoing(&self) {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
pub fn free_ongoing(&self) {
|
||||
let s_a = self.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = false;
|
||||
s.shall_stop_ongoing = true;
|
||||
s.cancel_sender.take();
|
||||
}
|
||||
|
||||
pub async fn has_ongoing(&self) -> bool {
|
||||
let s_a = &self.running_state;
|
||||
let s = s_a.read().await;
|
||||
pub fn has_ongoing(&self) -> bool {
|
||||
let s_a = self.running_state.clone();
|
||||
let s = s_a.read().unwrap();
|
||||
|
||||
s.ongoing_running || !s.shall_stop_ongoing
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
pub async fn stop_ongoing(&self) {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
if let Some(cancel) = s.cancel_sender.take() {
|
||||
cancel.send(()).await;
|
||||
}
|
||||
pub fn stop_ongoing(&self) {
|
||||
let s_a = self.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
if s.ongoing_running && !s.shall_stop_ongoing {
|
||||
info!(self, "Signaling the ongoing process to stop ASAP.",);
|
||||
@@ -243,69 +210,69 @@ impl Context {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn shall_stop_ongoing(&self) -> bool {
|
||||
self.running_state.read().await.shall_stop_ongoing
|
||||
pub fn shall_stop_ongoing(&self) -> bool {
|
||||
self.running_state
|
||||
.clone()
|
||||
.read()
|
||||
.unwrap()
|
||||
.shall_stop_ongoing
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* UI chat/message related API
|
||||
******************************************************************************/
|
||||
|
||||
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||
pub fn get_info(&self) -> HashMap<&'static str, String> {
|
||||
let unset = "0";
|
||||
let l = LoginParam::from_database(self, "").await;
|
||||
let l2 = LoginParam::from_database(self, "configured_").await;
|
||||
let displayname = self.get_config(Config::Displayname).await;
|
||||
let chats = get_chat_cnt(self).await as usize;
|
||||
let real_msgs = message::get_real_msg_cnt(self).await as usize;
|
||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
|
||||
let contacts = Contact::get_real_cnt(self).await as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await;
|
||||
let l = LoginParam::from_database(self, "");
|
||||
let l2 = LoginParam::from_database(self, "configured_");
|
||||
let displayname = self.get_config(Config::Displayname);
|
||||
let chats = get_chat_cnt(self) as usize;
|
||||
let real_msgs = message::get_real_msg_cnt(self) as usize;
|
||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize;
|
||||
let contacts = Contact::get_real_cnt(self) as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured);
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
|
||||
.await
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await;
|
||||
|
||||
let prv_key_cnt: Option<isize> = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await;
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled);
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
|
||||
let bcc_self = self.get_config_int(Config::BccSelf);
|
||||
|
||||
let pub_key_cnt: Option<isize> = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await;
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
let prv_key_cnt: Option<isize> =
|
||||
self.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS);
|
||||
|
||||
let pub_key_cnt: Option<isize> = self.sql.query_get_value(
|
||||
self,
|
||||
"SELECT COUNT(*) FROM acpeerstates;",
|
||||
rusqlite::NO_PARAMS,
|
||||
);
|
||||
|
||||
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
|
||||
key.fingerprint()
|
||||
} else {
|
||||
"<Not yet calculated>".into()
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch);
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch);
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch);
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove);
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "folders_configured")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let configured_sentbox_folder = self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.sql
|
||||
.get_raw_config(self, "configured_sentbox_folder")
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_mvbox_folder = self
|
||||
.get_config(Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.sql
|
||||
.get_raw_config(self, "configured_mvbox_folder")
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
|
||||
let mut res = get_info();
|
||||
@@ -315,13 +282,11 @@ impl Context {
|
||||
res.insert("number_of_contacts", contacts.to_string());
|
||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||
res.insert("database_version", dbversion.to_string());
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
|
||||
res.insert(
|
||||
"selfavatar",
|
||||
self.get_config(Config::Selfavatar)
|
||||
.await
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
@@ -347,14 +312,11 @@ impl Context {
|
||||
);
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
||||
let show_deaddrop: i32 = 0;
|
||||
pub fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
||||
let show_deaddrop = 0;
|
||||
self.sql
|
||||
.query_map(
|
||||
concat!(
|
||||
@@ -371,7 +333,7 @@ impl Context {
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
@@ -381,12 +343,11 @@ impl Context {
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
|
||||
pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
|
||||
let real_query = query.as_ref().trim();
|
||||
if real_query.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -426,7 +387,7 @@ impl Context {
|
||||
self.sql
|
||||
.query_map(
|
||||
query,
|
||||
paramsv![chat_id, strLikeInText, strLikeBeg],
|
||||
params![chat_id, &strLikeInText, &strLikeBeg],
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
@@ -436,34 +397,41 @@ impl Context {
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredInboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
folder_name.as_ref() == "INBOX"
|
||||
}
|
||||
|
||||
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredSentboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder");
|
||||
if let Some(name) = sentbox_name {
|
||||
name == folder_name.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder");
|
||||
|
||||
if let Some(name) = mvbox_name {
|
||||
name == folder_name.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
||||
if !self.get_config_bool(Config::MvboxMove).await {
|
||||
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
||||
if !self.get_config_bool(Config::MvboxMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_mvbox(folder).await {
|
||||
if self.is_mvbox(folder) {
|
||||
return;
|
||||
}
|
||||
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
|
||||
if let Ok(msg) = Message::load_from_db(self, msg_id) {
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
@@ -473,32 +441,30 @@ impl Context {
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => {}
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => {
|
||||
job::add(
|
||||
job_add(
|
||||
self,
|
||||
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
Action::MoveMsg,
|
||||
msg.id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
async fn is_io_running(&self) -> bool {
|
||||
self.scheduler.read().await.is_running()
|
||||
}
|
||||
|
||||
async fn stop_io(&self) {
|
||||
assert!(self.is_io_running().await, "context is already stopped");
|
||||
let token = {
|
||||
let lock = &*self.scheduler.read().await;
|
||||
lock.pre_stop().await
|
||||
};
|
||||
{
|
||||
let lock = &mut *self.scheduler.write().await;
|
||||
lock.stop(token).await;
|
||||
}
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
info!(self, "disconnecting inbox-thread",);
|
||||
self.inbox_thread.read().unwrap().imap.disconnect(self);
|
||||
info!(self, "disconnecting sentbox-thread",);
|
||||
self.sentbox_thread.read().unwrap().imap.disconnect(self);
|
||||
info!(self, "disconnecting mvbox-thread",);
|
||||
self.mvbox_thread.read().unwrap().imap.disconnect(self);
|
||||
info!(self, "disconnecting SMTP");
|
||||
self.smtp.clone().lock().unwrap().disconnect();
|
||||
self.sql.close(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +473,6 @@ impl Default for RunningState {
|
||||
RunningState {
|
||||
ongoing_running: false,
|
||||
shall_stop_ongoing: true,
|
||||
cancel_sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,6 +484,28 @@ pub(crate) struct BobStatus {
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum PerformJobsNeeded {
|
||||
Not,
|
||||
AtOnce,
|
||||
AvoidDos,
|
||||
}
|
||||
|
||||
impl Default for PerformJobsNeeded {
|
||||
fn default() -> Self {
|
||||
Self::Not
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SmtpState {
|
||||
pub idle: bool,
|
||||
pub suspended: bool,
|
||||
pub doing_jobs: bool,
|
||||
pub(crate) perform_jobs_needed: PerformJobsNeeded,
|
||||
pub probe_network: bool,
|
||||
}
|
||||
|
||||
pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
@@ -529,81 +516,81 @@ mod tests {
|
||||
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_wrong_db() {
|
||||
#[test]
|
||||
fn test_wrong_db() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
std::fs::write(&dbfile, b"123").unwrap();
|
||||
let res = Context::new("FakeOs".into(), dbfile.into()).await;
|
||||
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = dummy_context().await;
|
||||
let fresh = t.ctx.get_fresh_msgs().await;
|
||||
#[test]
|
||||
fn test_get_fresh_msgs() {
|
||||
let t = dummy_context();
|
||||
let fresh = t.ctx.get_fresh_msgs();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_blobdir_exists() {
|
||||
#[test]
|
||||
fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_wrong_blogdir() {
|
||||
#[test]
|
||||
fn test_wrong_blogdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
std::fs::write(&blobdir, b"123").unwrap();
|
||||
let res = Context::new("FakeOS".into(), dbfile.into()).await;
|
||||
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sqlite_parent_not_exists() {
|
||||
#[test]
|
||||
fn test_sqlite_parent_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_empty_blobdir() {
|
||||
#[test]
|
||||
fn test_with_empty_blobdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
|
||||
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_blobdir_not_exists() {
|
||||
#[test]
|
||||
fn test_with_blobdir_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
|
||||
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn no_crashes_on_context_deref() {
|
||||
let t = dummy_context();
|
||||
std::mem::drop(t.ctx);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_info() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_get_info() {
|
||||
let t = dummy_context();
|
||||
|
||||
let info = t.ctx.get_info().await;
|
||||
let info = t.ctx.get_info();
|
||||
assert!(info.get("database_dir").is_some());
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
379
src/dc_tools.rs
379
src/dc_tools.rs
@@ -3,27 +3,26 @@
|
||||
|
||||
use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::SystemTime;
|
||||
use std::{fmt, fs};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::{fs, io};
|
||||
use chrono::{Local, TimeZone};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
|
||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
0 != v && 0 == v & (v - 1)
|
||||
}
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
|
||||
/// the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
|
||||
let ellipse = if do_unwrap { "..." } else { "[...]" };
|
||||
|
||||
let count = buf.chars().count();
|
||||
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
||||
@@ -76,14 +75,6 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
|
||||
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
pub fn duration_to_str(duration: Duration) -> String {
|
||||
let secs = duration.as_secs();
|
||||
let h = secs / 3600;
|
||||
let m = (secs % 3600) / 60;
|
||||
let s = (secs % 3600) % 60;
|
||||
format!("{}h {}m {}s", h, m, s)
|
||||
}
|
||||
|
||||
pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
||||
the function may return negative values. */
|
||||
@@ -108,9 +99,9 @@ const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
||||
// returns the currently smeared timestamp,
|
||||
// may be used to check if call to dc_create_smeared_timestamp() is needed or not.
|
||||
// the returned timestamp MUST NOT be used to be sent out or saved in the database!
|
||||
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
|
||||
let mut now = time();
|
||||
let ts = *context.last_smeared_timestamp.read().await;
|
||||
let ts = *context.last_smeared_timestamp.read().unwrap();
|
||||
if ts >= now {
|
||||
now = ts + 1;
|
||||
}
|
||||
@@ -119,11 +110,11 @@ pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
|
||||
}
|
||||
|
||||
// returns a timestamp that is guaranteed to be unique.
|
||||
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||
pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||
let now = time();
|
||||
let mut ret = now;
|
||||
|
||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
|
||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
|
||||
if ret <= *last_smeared_timestamp {
|
||||
ret = *last_smeared_timestamp + 1;
|
||||
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
|
||||
@@ -138,12 +129,12 @@ pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
|
||||
// creates `count` timestamps that are guaranteed to be unique.
|
||||
// the frist created timestamps is returned directly,
|
||||
// get the other timestamps just by adding 1..count-1
|
||||
pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
|
||||
pub(crate) fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
|
||||
let now = time();
|
||||
let count = count as i64;
|
||||
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
|
||||
|
||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
|
||||
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
|
||||
start = max(*last_smeared_timestamp + 1, start);
|
||||
|
||||
*last_smeared_timestamp = start + count - 1;
|
||||
@@ -249,8 +240,11 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
|
||||
///
|
||||
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
|
||||
/// Otherwise, returns path as is.
|
||||
pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> PathBuf {
|
||||
let p: &Path = path.as_ref();
|
||||
pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
|
||||
context: &Context,
|
||||
path: P,
|
||||
) -> std::path::PathBuf {
|
||||
let p: &std::path::Path = path.as_ref();
|
||||
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
|
||||
context.get_blobdir().join(p)
|
||||
} else {
|
||||
@@ -258,20 +252,20 @@ pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> Pat
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
|
||||
pub(crate) fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> u64 {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
match fs::metadata(&path_abs).await {
|
||||
match fs::metadata(&path_abs) {
|
||||
Ok(meta) => meta.len() as u64,
|
||||
Err(_err) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
|
||||
pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
if !path_abs.exists().await {
|
||||
if !path_abs.exists() {
|
||||
return false;
|
||||
}
|
||||
if !path_abs.is_file().await {
|
||||
if !path_abs.is_file() {
|
||||
warn!(
|
||||
context,
|
||||
"refusing to delete non-file \"{}\".",
|
||||
@@ -281,9 +275,9 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
|
||||
}
|
||||
|
||||
let dpath = format!("{}", path.as_ref().to_string_lossy());
|
||||
match fs::remove_file(path_abs).await {
|
||||
match fs::remove_file(path_abs) {
|
||||
Ok(_) => {
|
||||
context.emit_event(Event::DeletedBlobFile(dpath));
|
||||
context.call_cb(Event::DeletedBlobFile(dpath));
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -293,13 +287,13 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_copy_file(
|
||||
pub(crate) fn dc_copy_file(
|
||||
context: &Context,
|
||||
src_path: impl AsRef<Path>,
|
||||
dest_path: impl AsRef<Path>,
|
||||
src_path: impl AsRef<std::path::Path>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> bool {
|
||||
let src_abs = dc_get_abs_path(context, &src_path);
|
||||
let mut src_file = match fs::File::open(&src_abs).await {
|
||||
let mut src_file = match fs::File::open(&src_abs) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -317,7 +311,6 @@ pub(crate) async fn dc_copy_file(
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&dest_abs)
|
||||
.await
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
@@ -331,7 +324,7 @@ pub(crate) async fn dc_copy_file(
|
||||
}
|
||||
};
|
||||
|
||||
match io::copy(&mut src_file, &mut dest_file).await {
|
||||
match std::io::copy(&mut src_file, &mut dest_file) {
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -343,20 +336,20 @@ pub(crate) async fn dc_copy_file(
|
||||
);
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
fs::remove_file(dest_abs).await.ok();
|
||||
fs::remove_file(dest_abs).ok();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_create_folder(
|
||||
pub(crate) fn dc_create_folder(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), io::Error> {
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
if !path_abs.exists().await {
|
||||
match fs::create_dir_all(path_abs).await {
|
||||
if !path_abs.exists() {
|
||||
match fs::create_dir_all(path_abs) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -374,13 +367,13 @@ pub(crate) async fn dc_create_folder(
|
||||
}
|
||||
|
||||
/// Write a the given content to provied file path.
|
||||
pub(crate) async fn dc_write_file(
|
||||
pub(crate) fn dc_write_file(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
buf: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
) -> Result<(), std::io::Error> {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
fs::write(&path_abs, buf).await.map_err(|err| {
|
||||
fs::write(&path_abs, buf).map_err(|err| {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot write {} bytes to \"{}\": {}",
|
||||
@@ -392,10 +385,13 @@ pub(crate) async fn dc_write_file(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
|
||||
pub fn dc_read_file<P: AsRef<std::path::Path>>(
|
||||
context: &Context,
|
||||
path: P,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
|
||||
match fs::read(&path_abs).await {
|
||||
match fs::read(&path_abs) {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -409,31 +405,13 @@ pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
|
||||
match fs::File::open(&path_abs).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot read \"{}\" or file is empty: {}",
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||
pub fn dc_open_file<P: AsRef<std::path::Path>>(
|
||||
context: &Context,
|
||||
path: P,
|
||||
) -> Result<std::fs::File, Error> {
|
||||
let p: PathBuf = path.as_ref().into();
|
||||
let path_abs = dc_get_abs_path(context, p);
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
|
||||
match std::fs::File::open(&path_abs) {
|
||||
match fs::File::open(&path_abs) {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -447,7 +425,7 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_get_next_backup_path(
|
||||
pub(crate) fn dc_get_next_backup_path(
|
||||
folder: impl AsRef<Path>,
|
||||
backup_time: i64,
|
||||
) -> Result<PathBuf, Error> {
|
||||
@@ -460,7 +438,7 @@ pub(crate) async fn dc_get_next_backup_path(
|
||||
for i in 0..64 {
|
||||
let mut path = folder.clone();
|
||||
path.push(format!("{}-{}.bak", stem, i));
|
||||
if !path.exists().await {
|
||||
if !path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
@@ -474,14 +452,6 @@ 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.
|
||||
@@ -505,7 +475,7 @@ pub struct EmailAddress {
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
|
||||
pub fn new(input: &str) -> Result<Self, Error> {
|
||||
input.parse::<EmailAddress>()
|
||||
}
|
||||
}
|
||||
@@ -517,62 +487,31 @@ impl fmt::Display for EmailAddress {
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = InvalidEmailError;
|
||||
type Err = Error;
|
||||
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
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() {
|
||||
return err("empty string is not valid");
|
||||
}
|
||||
fn from_str(input: &str) -> Result<EmailAddress, Error> {
|
||||
ensure!(!input.is_empty(), "empty string is not valid");
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
if input
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
return err("Email must not contain whitespaces, '>' or '<'");
|
||||
}
|
||||
ensure!(parts.len() > 1, "missing '@' character");
|
||||
let local = parts[1];
|
||||
let domain = parts[0];
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
return err("empty string is not valid for local part");
|
||||
}
|
||||
if domain.len() <= 3 {
|
||||
return err("domain is too short");
|
||||
}
|
||||
let dot = domain.find('.');
|
||||
match dot {
|
||||
None => {
|
||||
return err("invalid domain");
|
||||
}
|
||||
Some(dot_idx) => {
|
||||
if dot_idx >= domain.len() - 2 {
|
||||
return err("invalid domain");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => err("missing '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
ensure!(
|
||||
!local.is_empty(),
|
||||
"empty string is not valid for local part"
|
||||
);
|
||||
ensure!(domain.len() > 3, "domain is too short");
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
let dot = domain.find('.');
|
||||
ensure!(dot.is_some(), "invalid domain");
|
||||
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
|
||||
|
||||
Ok(EmailAddress {
|
||||
local: local.to_string(),
|
||||
domain: domain.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,42 +538,54 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate_1() {
|
||||
let s = "this is a little test string";
|
||||
assert_eq!(dc_truncate(s, 16), "this is a [...]");
|
||||
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
|
||||
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_2() {
|
||||
assert_eq!(dc_truncate("1234", 2), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2, false), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2, true), "1234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_3() {
|
||||
assert_eq!(dc_truncate("1234567", 1), "1[...]");
|
||||
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
|
||||
assert_eq!(dc_truncate("1234567", 1, true), "1...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_4() {
|
||||
assert_eq!(dc_truncate("123456", 4), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4, false), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4, true), "123456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_edge() {
|
||||
assert_eq!(dc_truncate("", 4), "");
|
||||
assert_eq!(dc_truncate("", 4, false), "");
|
||||
assert_eq!(dc_truncate("", 4, true), "");
|
||||
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
|
||||
|
||||
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
|
||||
"𐠈[...]"
|
||||
);
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
|
||||
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
||||
);
|
||||
|
||||
// 9 characters, so no truncation
|
||||
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
|
||||
);
|
||||
|
||||
// 12 characters, truncation
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A[...]",
|
||||
);
|
||||
}
|
||||
@@ -750,10 +701,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..10000usize
|
||||
approx_chars in 0..10000usize,
|
||||
do_unwrap: bool,
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let res = dc_truncate(&buf, approx_chars, do_unwrap);
|
||||
let el_len = if do_unwrap { 3 } else { 5 };
|
||||
let l = res.chars().count();
|
||||
if approx_chars > 0 {
|
||||
assert!(
|
||||
@@ -767,40 +719,40 @@ mod tests {
|
||||
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
if do_unwrap {
|
||||
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
|
||||
} else {
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_file_handling() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_file_handling() {
|
||||
let t = dummy_context();
|
||||
let context = &t.ctx;
|
||||
macro_rules! dc_file_exist {
|
||||
($ctx:expr, $fname:expr) => {
|
||||
$ctx.get_blobdir()
|
||||
.join(Path::new($fname).file_name().unwrap())
|
||||
.exists()
|
||||
};
|
||||
}
|
||||
let dc_file_exist = |ctx: &Context, fname: &str| {
|
||||
ctx.get_blobdir()
|
||||
.join(Path::new(fname).file_name().unwrap())
|
||||
.exists()
|
||||
};
|
||||
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
|
||||
if dc_file_exist!(context, "$BLOBDIR/foobar").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/dada").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await
|
||||
|| dc_file_exist!(context, "$BLOBDIR/foobar-folder").await
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje"));
|
||||
if dc_file_exist(context, "$BLOBDIR/foobar")
|
||||
|| dc_file_exist(context, "$BLOBDIR/dada")
|
||||
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|
||||
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
|
||||
{
|
||||
dc_delete_file(context, "$BLOBDIR/foobar").await;
|
||||
dc_delete_file(context, "$BLOBDIR/dada").await;
|
||||
dc_delete_file(context, "$BLOBDIR/foobar.dadada").await;
|
||||
dc_delete_file(context, "$BLOBDIR/foobar-folder").await;
|
||||
dc_delete_file(context, "$BLOBDIR/foobar");
|
||||
dc_delete_file(context, "$BLOBDIR/dada");
|
||||
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
|
||||
dc_delete_file(context, "$BLOBDIR/foobar-folder");
|
||||
}
|
||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await);
|
||||
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await);
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
|
||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok());
|
||||
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
|
||||
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7);
|
||||
|
||||
let abs_path = context
|
||||
.get_blobdir()
|
||||
@@ -808,33 +760,31 @@ mod tests {
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
assert!(dc_file_exist!(context, &abs_path).await);
|
||||
assert!(dc_file_exist(context, &abs_path));
|
||||
|
||||
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
||||
|
||||
// attempting to copy a second time should fail
|
||||
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
||||
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
|
||||
|
||||
let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
|
||||
let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap();
|
||||
|
||||
assert_eq!(buf.len(), 7);
|
||||
assert_eq!(&buf, b"content");
|
||||
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
|
||||
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await);
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
|
||||
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok());
|
||||
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));
|
||||
|
||||
let fn0 = "$BLOBDIR/data.data";
|
||||
assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
|
||||
assert!(dc_write_file(context, &fn0, b"content").is_ok());
|
||||
|
||||
assert!(dc_delete_file(context, &fn0).await);
|
||||
assert!(!dc_file_exist!(context, &fn0).await);
|
||||
assert!(dc_delete_file(context, &fn0));
|
||||
assert!(!dc_file_exist(context, &fn0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -851,15 +801,15 @@ mod tests {
|
||||
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_smeared_timestamp() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_smeared_timestamp() {
|
||||
let t = dummy_context();
|
||||
assert_ne!(
|
||||
dc_create_smeared_timestamp(&t.ctx).await,
|
||||
dc_create_smeared_timestamp(&t.ctx).await
|
||||
dc_create_smeared_timestamp(&t.ctx),
|
||||
dc_create_smeared_timestamp(&t.ctx)
|
||||
);
|
||||
assert!(
|
||||
dc_create_smeared_timestamp(&t.ctx).await
|
||||
dc_create_smeared_timestamp(&t.ctx)
|
||||
>= SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -867,50 +817,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_smeared_timestamps() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_smeared_timestamps() {
|
||||
let t = dummy_context();
|
||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||
let next = dc_smeared_time(&t.ctx).await;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize);
|
||||
let next = dc_smeared_time(&t.ctx);
|
||||
assert!((start + count - 1) < next);
|
||||
|
||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||
let next = dc_smeared_time(&t.ctx).await;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize);
|
||||
let next = dc_smeared_time(&t.ctx);
|
||||
assert!((start + count - 1) < next);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_to_str() {
|
||||
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
|
||||
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(59 * 60 + 59)),
|
||||
"0h 59m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(59 * 60 + 60)),
|
||||
"1h 0m 0s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
|
||||
"2h 59m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
|
||||
"3h 0m 0s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
|
||||
"3h 0m 59s"
|
||||
);
|
||||
assert_eq!(
|
||||
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
|
||||
"3h 1m 0s"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
lazy_static! {
|
||||
@@ -34,7 +35,6 @@ pub fn dehtml(buf: &str) -> String {
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
reader.check_end_names(false);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
@@ -225,23 +225,4 @@ mod tests {
|
||||
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unclosed_tags() {
|
||||
let input = r##"
|
||||
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
|
||||
'http://www.w3.org/TR/html4/loose.dtd'>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hi</title>
|
||||
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
|
||||
</head>
|
||||
<body>
|
||||
lots of text
|
||||
</body>
|
||||
</html>
|
||||
"##;
|
||||
let txt = dehtml(input);
|
||||
assert_eq!(txt.trim(), "lots of text");
|
||||
}
|
||||
}
|
||||
|
||||
253
src/e2ee.rs
253
src/e2ee.rs
@@ -1,17 +1,19 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::error::*;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::headerdef::HeaderDefMap;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::pgp;
|
||||
@@ -25,18 +27,18 @@ pub struct EncryptHelper {
|
||||
}
|
||||
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
pub fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled))
|
||||
.unwrap_or_default();
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
let addr = match context.get_config(Config::ConfiguredAddr) {
|
||||
None => {
|
||||
bail!("addr not configured!");
|
||||
}
|
||||
Some(addr) => addr,
|
||||
};
|
||||
|
||||
let public_key = SignedPublicKey::load_self(context).await?;
|
||||
let public_key = load_or_generate_self_public_key(context, &addr)?;
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
@@ -86,44 +88,46 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
pub fn encrypt(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
min_verified: PeerstateVerifiedStatus,
|
||||
mail_to_encrypt: lettre_email::PartBuilder,
|
||||
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
|
||||
peerstates: &[(Option<Peerstate>, &str)],
|
||||
) -> Result<String> {
|
||||
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut keyring = Keyring::default();
|
||||
|
||||
for (peerstate, addr) in peerstates
|
||||
.into_iter()
|
||||
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
|
||||
.iter()
|
||||
.filter_map(|(state, addr)| state.as_ref().map(|s| (s, addr)))
|
||||
{
|
||||
let key = peerstate.take_key(min_verified).ok_or_else(|| {
|
||||
let key = peerstate.peek_key(min_verified).ok_or_else(|| {
|
||||
format_err!("proper enc-key for {} missing, cannot encrypt", addr)
|
||||
})?;
|
||||
keyring.add(key);
|
||||
keyring.add_ref(key);
|
||||
}
|
||||
keyring.add(self.public_key.clone());
|
||||
let sign_key = SignedSecretKey::load_self(context).await?;
|
||||
let public_key = Key::from(self.public_key.clone());
|
||||
keyring.add_ref(&public_key);
|
||||
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
|
||||
.ok_or_else(|| format_err!("missing own private key"))?;
|
||||
|
||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||
|
||||
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?;
|
||||
let ctext = pgp::pk_encrypt(&raw_message, &keyring, Some(&sign_key))?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_decrypt(
|
||||
pub fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
message_time: i64,
|
||||
) -> Result<(Option<Vec<u8>>, HashSet<Fingerprint>)> {
|
||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
|
||||
.get_header_value(HeaderDef::From_)?
|
||||
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
@@ -132,54 +136,96 @@ pub async fn try_decrypt(
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await;
|
||||
peerstate = Peerstate::from_addr(context, &context.sql, &from);
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
peerstate.apply_header(&header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
peerstate.save_to_db(&context.sql, false)?;
|
||||
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
peerstate.save_to_db(&context.sql, false)?;
|
||||
}
|
||||
} else if let Some(ref header) = autocryptheader {
|
||||
let p = Peerstate::from_header(context, header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
p.save_to_db(&context.sql, true)?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
/* possibly perform decryption */
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut private_keyring = Keyring::default();
|
||||
let mut public_keyring_for_validate = Keyring::default();
|
||||
let mut out_mail = None;
|
||||
let mut signatures = HashSet::default();
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr);
|
||||
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await;
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
handle_degrade_event(context, &peerstate).await?;
|
||||
}
|
||||
if let Some(key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
}
|
||||
if let Some(key) = peerstate.public_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
if let Some(self_addr) = self_addr {
|
||||
if private_keyring.load_self_private_for_decrypting(context, self_addr, &context.sql) {
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &context.sql, &from);
|
||||
}
|
||||
if let Some(ref peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
handle_degrade_event(context, &peerstate)?;
|
||||
}
|
||||
if let Some(ref key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add_ref(key);
|
||||
}
|
||||
if let Some(ref key) = peerstate.public_key {
|
||||
public_keyring_for_validate.add_ref(key);
|
||||
}
|
||||
}
|
||||
|
||||
out_mail = decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
&private_keyring,
|
||||
&public_keyring_for_validate,
|
||||
&mut signatures,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
let out_mail = decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
&mut signatures,
|
||||
)
|
||||
.await?;
|
||||
Ok((out_mail, signatures))
|
||||
}
|
||||
|
||||
/// Load public key from database or generate a new one.
|
||||
///
|
||||
/// This will load a public key from the database, generating and
|
||||
/// storing a new one when one doesn't exist yet. Care is taken to
|
||||
/// only generate one key per context even when multiple threads call
|
||||
/// this function concurrently.
|
||||
fn load_or_generate_self_public_key(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
) -> Result<SignedPublicKey> {
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
let _guard = context.generating_key_mutex.lock().unwrap();
|
||||
|
||||
// Check again in case the key was generated while we were waiting for the lock.
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let keygen_type =
|
||||
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keygen_type);
|
||||
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
|
||||
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair.public)
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
||||
ensure!(
|
||||
@@ -207,12 +253,12 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
|
||||
Ok(&mail.subparts[1])
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
fn decrypt_if_autocrypt_message<'a>(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'a>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
private_keyring: &Keyring,
|
||||
public_keyring_for_validate: &Keyring,
|
||||
ret_valid_signatures: &mut HashSet<String>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
// The returned bool is true if we detected an Autocrypt-encrypted
|
||||
// message and successfully decrypted it. Decryption then modifies the
|
||||
@@ -231,20 +277,21 @@ async fn decrypt_if_autocrypt_message<'a>(
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
|
||||
decrypt_part(
|
||||
context,
|
||||
encrypted_data_part,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
ret_valid_signatures,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
async fn decrypt_part(
|
||||
fn decrypt_part(
|
||||
_context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
private_keyring: &Keyring,
|
||||
public_keyring_for_validate: &Keyring,
|
||||
ret_valid_signatures: &mut HashSet<String>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
@@ -253,12 +300,11 @@ async fn decrypt_part(
|
||||
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
||||
|
||||
let plain = pgp::pk_decrypt(
|
||||
data,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
&data,
|
||||
&private_keyring,
|
||||
&public_keyring_for_validate,
|
||||
Some(ret_valid_signatures),
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
|
||||
return Ok(Some(plain));
|
||||
@@ -301,18 +347,14 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||
///
|
||||
/// If this succeeds you are also guaranteed that the
|
||||
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
||||
// TODO, remove this once deltachat::key::Key no longer exists.
|
||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
"Failed to get self address, ",
|
||||
"cannot ensure secret key if not configured."
|
||||
))
|
||||
})?;
|
||||
SignedPublicKey::load_self(context).await?;
|
||||
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
"Failed to get self address, ",
|
||||
"cannot ensure secret key if not configured."
|
||||
))
|
||||
})?;
|
||||
load_or_generate_self_public_key(context, &self_addr)?;
|
||||
Ok(self_addr)
|
||||
}
|
||||
|
||||
@@ -325,17 +367,17 @@ mod tests {
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prexisting() {
|
||||
let t = dummy_context().await;
|
||||
let test_addr = configure_alice_keypair(&t.ctx).await;
|
||||
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
|
||||
#[test]
|
||||
fn test_prexisting() {
|
||||
let t = dummy_context();
|
||||
let test_addr = configure_alice_keypair(&t.ctx);
|
||||
assert_eq!(ensure_secret_key_exists(&t.ctx).unwrap(), test_addr);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_not_configured() {
|
||||
let t = dummy_context().await;
|
||||
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
|
||||
#[test]
|
||||
fn test_not_configured() {
|
||||
let t = dummy_context();
|
||||
assert!(ensure_secret_key_exists(&t.ctx).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +405,49 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
);
|
||||
}
|
||||
|
||||
mod load_or_generate_self_public_key {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_existing() {
|
||||
let t = dummy_context();
|
||||
let addr = configure_alice_keypair(&t.ctx);
|
||||
let key = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_generate() {
|
||||
let t = dummy_context();
|
||||
let addr = "alice@example.org";
|
||||
let key0 = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key0.is_ok());
|
||||
let key1 = load_or_generate_self_public_key(&t.ctx, addr);
|
||||
assert!(key1.is_ok());
|
||||
assert_eq!(key0.unwrap(), key1.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_generate_concurrent() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let t = dummy_context();
|
||||
let ctx = Arc::new(t.ctx);
|
||||
let ctx0 = Arc::clone(&ctx);
|
||||
let thr0 =
|
||||
thread::spawn(move || load_or_generate_self_public_key(&ctx0, "alice@example.org"));
|
||||
let ctx1 = Arc::clone(&ctx);
|
||||
let thr1 =
|
||||
thread::spawn(move || load_or_generate_self_public_key(&ctx1, "alice@example.org"));
|
||||
let res0 = thr0.join().unwrap();
|
||||
let res1 = thr1.join().unwrap();
|
||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_decrypted_pgp_armor() {
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
|
||||
185
src/error.rs
185
src/error.rs
@@ -1,6 +1,189 @@
|
||||
//! # Error handling
|
||||
|
||||
pub use anyhow::{bail, ensure, format_err, Error, Result};
|
||||
use lettre_email::mime;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Failure(failure::Error),
|
||||
|
||||
#[fail(display = "SQL error: {:?}", _0)]
|
||||
SqlError(#[cause] crate::sql::Error),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Io(std::io::Error),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Message(String),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Image(image_meta::ImageError),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Utf8(std::str::Utf8Error),
|
||||
|
||||
#[fail(display = "PGP: {:?}", _0)]
|
||||
Pgp(pgp::errors::Error),
|
||||
|
||||
#[fail(display = "Base64Decode: {:?}", _0)]
|
||||
Base64Decode(base64::DecodeError),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
FromUtf8(std::string::FromUtf8Error),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
BlobError(#[cause] crate::blob::BlobError),
|
||||
|
||||
#[fail(display = "Invalid Message ID.")]
|
||||
InvalidMsgId,
|
||||
|
||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
||||
WatchFolderNotFound(String),
|
||||
|
||||
#[fail(display = "Invalid Email: {:?}", _0)]
|
||||
MailParseError(#[cause] mailparse::MailParseError),
|
||||
|
||||
#[fail(display = "Building invalid Email: {:?}", _0)]
|
||||
LettreError(#[cause] lettre_email::error::Error),
|
||||
|
||||
#[fail(display = "SMTP error: {:?}", _0)]
|
||||
SmtpError(#[cause] async_smtp::error::Error),
|
||||
|
||||
#[fail(display = "FromStr error: {:?}", _0)]
|
||||
FromStr(#[cause] mime::FromStrError),
|
||||
|
||||
#[fail(display = "Not Configured")]
|
||||
NotConfigured,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<crate::sql::Error> for Error {
|
||||
fn from(err: crate::sql::Error) -> Error {
|
||||
Error::SqlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Error {
|
||||
Error::Base64Decode(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<failure::Error> for Error {
|
||||
fn from(err: failure::Error) -> Error {
|
||||
Error::Failure(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::Utf8Error> for Error {
|
||||
fn from(err: std::str::Utf8Error) -> Error {
|
||||
Error::Utf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image_meta::ImageError> for Error {
|
||||
fn from(err: image_meta::ImageError) -> Error {
|
||||
Error::Image(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::errors::Error> for Error {
|
||||
fn from(err: pgp::errors::Error) -> Error {
|
||||
Error::Pgp(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(err: std::string::FromUtf8Error) -> Error {
|
||||
Error::FromUtf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::blob::BlobError> for Error {
|
||||
fn from(err: crate::blob::BlobError) -> Error {
|
||||
Error::BlobError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::message::InvalidMsgId> for Error {
|
||||
fn from(_err: crate::message::InvalidMsgId) -> Error {
|
||||
Error::InvalidMsgId
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::key::SaveKeyError> for Error {
|
||||
fn from(err: crate::key::SaveKeyError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::pgp::PgpKeygenError> for Error {
|
||||
fn from(err: crate::pgp::PgpKeygenError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mailparse::MailParseError> for Error {
|
||||
fn from(err: mailparse::MailParseError) -> Error {
|
||||
Error::MailParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre_email::error::Error> for Error {
|
||||
fn from(err: lettre_email::error::Error) -> Error {
|
||||
Error::LettreError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mime::FromStrError> for Error {
|
||||
fn from(err: mime::FromStrError) -> Error {
|
||||
Error::FromStr(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($e:expr) => {
|
||||
return Err($crate::error::Error::Message($e.to_string()));
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! format_err {
|
||||
($e:expr) => {
|
||||
$crate::error::Error::Message($e.to_string());
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::error::Error::Message(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $e:expr) => {
|
||||
if !($cond) {
|
||||
bail!($e);
|
||||
}
|
||||
};
|
||||
($cond:expr, $fmt:expr, $($arg:tt)+) => {
|
||||
if !($cond) {
|
||||
bail!($fmt, $($arg)+);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_eq {
|
||||
|
||||
@@ -1,65 +1,12 @@
|
||||
//! # Events specification
|
||||
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::sync::{channel, Receiver, Sender, TrySendError};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::message::MsgId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Events {
|
||||
receiver: Receiver<Event>,
|
||||
sender: Sender<Event>,
|
||||
}
|
||||
|
||||
impl Default for Events {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = channel(1_000);
|
||||
|
||||
Self { receiver, sender }
|
||||
}
|
||||
}
|
||||
|
||||
impl Events {
|
||||
pub fn emit(&self, event: Event) {
|
||||
match self.sender.try_send(event) {
|
||||
Ok(()) => {}
|
||||
Err(TrySendError::Full(event)) => {
|
||||
// when we are full, we pop remove the oldest event and push on the new one
|
||||
let _ = self.receiver.try_recv();
|
||||
|
||||
// try again
|
||||
self.emit(event);
|
||||
}
|
||||
Err(TrySendError::Disconnected(_)) => {
|
||||
unreachable!("unable to emit event, channel disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the event emitter.
|
||||
pub fn get_emitter(&self) -> EventEmitter {
|
||||
EventEmitter(self.receiver.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventEmitter(Receiver<Event>);
|
||||
|
||||
impl EventEmitter {
|
||||
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub fn recv_sync(&self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv())
|
||||
}
|
||||
|
||||
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
// TODO: change once we can use async channels internally.
|
||||
self.0.recv().await.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Returns the corresponding Event id.
|
||||
pub fn as_id(&self) -> i32 {
|
||||
@@ -254,4 +201,10 @@ pub enum Event {
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// This event is sent out to the inviter when a joiner successfully joined a group.
|
||||
/// @param data1 (int) chat_id
|
||||
/// @param data2 (int) contact_id
|
||||
#[strum(props(id = "2062"))]
|
||||
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::strum::AsStaticRef;
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
@@ -34,7 +34,6 @@ pub enum HeaderDef {
|
||||
ChatContent,
|
||||
ChatDuration,
|
||||
ChatDispositionNotificationTo,
|
||||
ChatUploadUrl,
|
||||
Autocrypt,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
@@ -53,17 +52,13 @@ impl HeaderDef {
|
||||
}
|
||||
|
||||
pub trait HeaderDefMap {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
|
||||
}
|
||||
|
||||
impl HeaderDefMap for [MailHeader<'_>] {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
|
||||
self.get_first_value(headerdef.get_headername())
|
||||
}
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
|
||||
self.get_first_header(headerdef.get_headername())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -84,13 +79,18 @@ mod tests {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.unwrap(),
|
||||
Some("v99".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
headers.get_header_value(HeaderDef::From_).unwrap(),
|
||||
Some("Bob".to_string())
|
||||
);
|
||||
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,28 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use async_imap::{
|
||||
error::{Error as ImapError, Result as ImapResult},
|
||||
Client as ImapClient,
|
||||
};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::{self, TcpStream};
|
||||
|
||||
use super::session::Session;
|
||||
use crate::login_param::dc_build_tls;
|
||||
|
||||
use super::session::SessionStream;
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
is_secure: bool,
|
||||
inner: ImapClient<Box<dyn SessionStream>>,
|
||||
}
|
||||
|
||||
impl Deref for Client {
|
||||
type Target = ImapClient<Box<dyn SessionStream>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Client {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
pub(crate) enum Client {
|
||||
Secure(ImapClient<TlsStream<TcpStream>>),
|
||||
Insecure(ImapClient<TcpStream>),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||
self,
|
||||
username: U,
|
||||
password: P,
|
||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||
let Client { inner, is_secure } = self;
|
||||
let session = inner
|
||||
.login(username, password)
|
||||
.await
|
||||
.map_err(|(err, client)| {
|
||||
(
|
||||
err,
|
||||
Client {
|
||||
is_secure,
|
||||
inner: client,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
Ok(Session { inner: session })
|
||||
}
|
||||
|
||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||
self,
|
||||
auth_type: S,
|
||||
authenticator: &A,
|
||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||
let Client { inner, is_secure } = self;
|
||||
let session =
|
||||
inner
|
||||
.authenticate(auth_type, authenticator)
|
||||
.await
|
||||
.map_err(|(err, client)| {
|
||||
(
|
||||
err,
|
||||
Client {
|
||||
is_secure,
|
||||
inner: client,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
Ok(Session { inner: session })
|
||||
}
|
||||
|
||||
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
|
||||
addr: A,
|
||||
domain: S,
|
||||
strict_tls: bool,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let tls = dc_build_tls(strict_tls);
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(domain.as_ref(), stream).await?);
|
||||
let tls = dc_build_tls(certificate_checks);
|
||||
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
@@ -94,14 +33,11 @@ impl Client {
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client::Secure(client))
|
||||
}
|
||||
|
||||
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
|
||||
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
@@ -112,28 +48,57 @@ impl Client {
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: false,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client::Insecure(client))
|
||||
}
|
||||
|
||||
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
|
||||
if self.is_secure {
|
||||
Ok(self)
|
||||
} else {
|
||||
let Client { mut inner, .. } = self;
|
||||
let tls = dc_build_tls(strict_tls);
|
||||
inner.run_command_and_check_ok("STARTTLS", None).await?;
|
||||
pub async fn secure<S: AsRef<str>>(
|
||||
self,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Client> {
|
||||
match self {
|
||||
Client::Insecure(client) => {
|
||||
let tls = dc_build_tls(certificate_checks);
|
||||
let client_sec = client.secure(domain, tls).await?;
|
||||
|
||||
let stream = inner.into_inner();
|
||||
let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
|
||||
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
|
||||
Ok(Client::Secure(client_sec))
|
||||
}
|
||||
// Nothing to do
|
||||
Client::Secure(_) => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: ImapClient::new(boxed),
|
||||
})
|
||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||
self,
|
||||
auth_type: S,
|
||||
authenticator: &A,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||
self,
|
||||
username: U,
|
||||
password: P,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
452
src/imap/idle.rs
452
src/imap/idle.rs
@@ -1,231 +1,299 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
use crate::context::Context;
|
||||
|
||||
use super::select_folder;
|
||||
use super::session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
#[fail(display = "IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[cause] async_std::future::TimeoutError),
|
||||
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
|
||||
#[error("Setup handle error")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
#[fail(display = "Setup handle error")]
|
||||
SetupHandleError(#[cause] super::Error),
|
||||
}
|
||||
|
||||
impl From<select_folder::Error> for Error {
|
||||
fn from(err: select_folder::Error) -> Error {
|
||||
Error::SelectFolderError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum IdleHandle {
|
||||
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
|
||||
Insecure(ImapIdleHandle<TcpStream>),
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn idle(self) -> IdleHandle {
|
||||
match self {
|
||||
Session::Secure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Secure(h)
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Insecure(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub fn can_idle(&self) -> bool {
|
||||
self.config.can_idle
|
||||
task::block_on(async move { self.config.read().await.can_idle })
|
||||
}
|
||||
|
||||
pub async fn idle(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
watch_folder: Option<String>,
|
||||
) -> Result<InterruptInfo> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
if !self.can_idle() {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
}
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
let session = self.session.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if let Some(session) = session {
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.can_idle() {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
|
||||
enum Event {
|
||||
IdleResponse(IdleResponse),
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
drop(idle_wait);
|
||||
drop(interrupt);
|
||||
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
|
||||
self.idle_interrupt.recv().map(|probe_network| {
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
}),
|
||||
);
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info = i;
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res = handle
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
self.setup_handle_if_needed(context)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
self.trigger_reconnect();
|
||||
Error::IdleTimeout(err)
|
||||
})?;
|
||||
.map_err(Error::SetupHandleError)?;
|
||||
|
||||
match res {
|
||||
Ok(session) => {
|
||||
self.session = Some(Session { inner: session });
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
let session = self.session.lock().await.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
if let Some(session) = session {
|
||||
match session.idle() {
|
||||
// BEWARE: If you change the Secure branch you
|
||||
// typically also need to change the Insecure branch.
|
||||
IdleHandle::Secure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
pub(crate) async fn fake_idle(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
watch_folder: Option<String>,
|
||||
) -> InterruptInfo {
|
||||
// Idle using polling. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job (and an interrupt).
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
let fake_idle_start_time = SystemTime::now();
|
||||
info!(context, "IMAP-fake-IDLEing...");
|
||||
|
||||
// Do not poll, just wait for an interrupt when no folder is passed in.
|
||||
if watch_folder.is_none() {
|
||||
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||
}
|
||||
|
||||
let mut info: InterruptInfo = Default::default();
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
info =
|
||||
loop {
|
||||
use futures::future::FutureExt;
|
||||
match interval
|
||||
.next()
|
||||
.map(|_| Event::Tick)
|
||||
.race(self.idle_interrupt.recv().map(|probe_network| {
|
||||
Event::Interrupt(probe_network.unwrap_or_default())
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Event::Tick => {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
Ok(IdleResponse::NewData(_)) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
Ok(IdleResponse::Timeout) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(IdleResponse::ManualInterrupt) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res =
|
||||
async_std::future::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
self.trigger_reconnect();
|
||||
Error::IdleTimeout(err)
|
||||
})?;
|
||||
|
||||
match res {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Secure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
IdleHandle::Insecure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IMAP-fake-IDLE done after {:.4}s",
|
||||
SystemTime::now()
|
||||
.duration_since(fake_idle_start_time)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as f64
|
||||
/ 1000.,
|
||||
);
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
info
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
Ok(IdleResponse::NewData(_)) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
Ok(IdleResponse::Timeout) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(IdleResponse::ManualInterrupt) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res =
|
||||
async_std::future::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
self.trigger_reconnect();
|
||||
Error::IdleTimeout(err)
|
||||
})?;
|
||||
|
||||
match res {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Insecure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
|
||||
// Idle using polling. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job (and an interrupt).
|
||||
task::block_on(async move {
|
||||
let fake_idle_start_time = SystemTime::now();
|
||||
|
||||
info!(context, "IMAP-fake-IDLEing...");
|
||||
|
||||
let interrupt = stop_token::StopSource::new();
|
||||
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// loop until we are interrupted or if we fetched something
|
||||
while let Some(_) = interrupt_interval.next().await {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context) {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.read().await.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interrupt.lock().await.take();
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IMAP-fake-IDLE done after {:.4}s",
|
||||
SystemTime::now()
|
||||
.duration_since(fake_idle_start_time)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as f64
|
||||
/ 1000.,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn interrupt_idle(&self, context: &Context) {
|
||||
task::block_on(async move {
|
||||
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
|
||||
if interrupt.is_none() {
|
||||
// idle wait is not running, signal it needs to skip
|
||||
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
|
||||
|
||||
// meanwhile idle-wait may have produced the StopSource
|
||||
interrupt = self.interrupt.lock().await.take();
|
||||
}
|
||||
// let's manually drop the StopSource
|
||||
if interrupt.is_some() {
|
||||
// the imap thread provided us a stop token but might
|
||||
// not have entered idle_wait yet, give it some time
|
||||
// for that to happen. XXX handle this without extra wait
|
||||
// https://github.com/deltachat/deltachat-core-rust/issues/925
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
info!(context, "low-level: dropping stop-source to interrupt idle");
|
||||
std::mem::drop(interrupt)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1707
src/imap/mod.rs
1707
src/imap/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -4,63 +4,36 @@ use crate::context::Context;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[error("IMAP Could not obtain imap-session object.")]
|
||||
#[fail(display = "IMAP Could not obtain imap-session object.")]
|
||||
NoSession,
|
||||
|
||||
#[error("IMAP Connection Lost or no connection established")]
|
||||
#[fail(display = "IMAP Connection Lost or no connection established")]
|
||||
ConnectionLost,
|
||||
|
||||
#[error("IMAP Folder name invalid: {0}")]
|
||||
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
|
||||
BadFolderName(String),
|
||||
|
||||
#[error("IMAP close/expunge failed")]
|
||||
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||
#[fail(display = "IMAP close/expunge failed: {}", _0)]
|
||||
CloseExpungeFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP other error: {0}")]
|
||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
/// Issues a CLOSE command to expunge selected folder.
|
||||
///
|
||||
/// CLOSE is considerably faster than an EXPUNGE, see
|
||||
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
||||
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
|
||||
if let Some(ref folder) = self.config.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
if let Some(ref mut session) = self.session {
|
||||
match session.close().await {
|
||||
Ok(_) => {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
}
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||
&mut self,
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: Option<S>,
|
||||
) -> Result<()> {
|
||||
if self.session.is_none() {
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
if self.session.lock().await.is_none() {
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.selected_folder = None;
|
||||
cfg.selected_folder_needs_expunge = false;
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
@@ -68,7 +41,7 @@ impl Imap {
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref selected_folder) = self.config.selected_folder {
|
||||
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
|
||||
if folder.as_ref() == selected_folder {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -76,14 +49,33 @@ impl Imap {
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
let needs_expunge = { self.config.selected_folder_needs_expunge };
|
||||
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
|
||||
if needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
if let Some(ref folder) = self.config.read().await.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
|
||||
// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.close().await {
|
||||
Ok(_) => {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
}
|
||||
self.config.write().await.selected_folder_needs_expunge = false;
|
||||
}
|
||||
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
let res = session.select(folder).await;
|
||||
|
||||
// https://tools.ietf.org/html/rfc3501#section-6.3.1
|
||||
@@ -92,20 +84,21 @@ impl Imap {
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.config.selected_folder = Some(folder.as_ref().to_string());
|
||||
self.config.selected_mailbox = Some(mailbox);
|
||||
let mut config = self.config.write().await;
|
||||
config.selected_folder = Some(folder.as_ref().to_string());
|
||||
config.selected_mailbox = Some(mailbox);
|
||||
Ok(())
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => {
|
||||
self.trigger_reconnect();
|
||||
self.config.selected_folder = None;
|
||||
self.config.write().await.selected_folder = None;
|
||||
Err(Error::ConnectionLost)
|
||||
}
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.as_ref().to_string()))
|
||||
}
|
||||
Err(err) => {
|
||||
self.config.selected_folder = None;
|
||||
self.config.write().await.selected_folder = None;
|
||||
self.trigger_reconnect();
|
||||
Err(Error::Other(err.to_string()))
|
||||
}
|
||||
|
||||
@@ -1,40 +1,172 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use async_imap::Session as ImapSession;
|
||||
use async_imap::{
|
||||
error::Result as ImapResult,
|
||||
types::{Capabilities, Fetch, Mailbox, Name},
|
||||
Session as ImapSession,
|
||||
};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Session {
|
||||
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
|
||||
}
|
||||
|
||||
pub(crate) trait SessionStream:
|
||||
async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug
|
||||
{
|
||||
}
|
||||
|
||||
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
|
||||
impl SessionStream for TlsStream<TcpStream> {}
|
||||
impl SessionStream for TcpStream {}
|
||||
|
||||
impl Deref for Session {
|
||||
type Target = ImapSession<Box<dyn SessionStream>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Session {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
pub(crate) enum Session {
|
||||
Secure(ImapSession<TlsStream<TcpStream>>),
|
||||
Insecure(ImapSession<TcpStream>),
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
|
||||
let Session { inner } = self;
|
||||
inner.idle()
|
||||
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
|
||||
let res = match self {
|
||||
Session::Secure(i) => i.capabilities().await?,
|
||||
Session::Insecure(i) => i.capabilities().await?,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
&mut self,
|
||||
reference_name: Option<&str>,
|
||||
mailbox_pattern: Option<&str>,
|
||||
) -> ImapResult<Vec<Name>> {
|
||||
let res = match self {
|
||||
Session::Secure(i) => {
|
||||
i.list(reference_name, mailbox_pattern)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
i.list(reference_name, mailbox_pattern)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<()> {
|
||||
match self {
|
||||
Session::Secure(i) => i.create(mailbox_name).await?,
|
||||
Session::Insecure(i) => i.create(mailbox_name).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> ImapResult<()> {
|
||||
match self {
|
||||
Session::Secure(i) => i.subscribe(mailbox).await?,
|
||||
Session::Insecure(i) => i.subscribe(mailbox).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn close(&mut self) -> ImapResult<()> {
|
||||
match self {
|
||||
Session::Secure(i) => i.close().await?,
|
||||
Session::Insecure(i) => i.close().await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<Mailbox> {
|
||||
let mbox = match self {
|
||||
Session::Secure(i) => i.select(mailbox_name).await?,
|
||||
Session::Insecure(i) => i.select(mailbox_name).await?,
|
||||
};
|
||||
|
||||
Ok(mbox)
|
||||
}
|
||||
|
||||
pub async fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
let res = match self {
|
||||
Session::Secure(i) => {
|
||||
i.fetch(sequence_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
i.fetch(sequence_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
let res = match self {
|
||||
Session::Secure(i) => {
|
||||
i.uid_fetch(uid_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
i.uid_fetch(uid_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
let res = match self {
|
||||
Session::Secure(i) => {
|
||||
i.uid_store(uid_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
i.uid_store(uid_set, query)
|
||||
.await?
|
||||
.collect::<ImapResult<_>>()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
&mut self,
|
||||
uid_set: S1,
|
||||
mailbox_name: S2,
|
||||
) -> ImapResult<()> {
|
||||
match self {
|
||||
Session::Secure(i) => i.uid_mv(uid_set, mailbox_name).await?,
|
||||
Session::Insecure(i) => i.uid_mv(uid_set, mailbox_name).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
&mut self,
|
||||
uid_set: S1,
|
||||
mailbox_name: S2,
|
||||
) -> ImapResult<()> {
|
||||
match self {
|
||||
Session::Secure(i) => i.uid_copy(uid_set, mailbox_name).await?,
|
||||
Session::Insecure(i) => i.uid_copy(uid_set, mailbox_name).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
595
src/imex.rs
595
src/imex.rs
@@ -1,10 +1,9 @@
|
||||
//! # Import/export module
|
||||
|
||||
use std::any::Any;
|
||||
use std::cmp::{max, min};
|
||||
use core::cmp::{max, min};
|
||||
use std::path::Path;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -17,7 +16,8 @@ use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::error::*;
|
||||
use crate::events::Event;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::job::*;
|
||||
use crate::key::{self, Key};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
@@ -53,10 +53,13 @@ pub enum ImexMode {
|
||||
}
|
||||
|
||||
/// Import/export things.
|
||||
/// For this purpose, the function creates a job that is executed in the IMAP-thread then;
|
||||
/// this requires to call dc_perform_inbox_jobs() regularly.
|
||||
///
|
||||
/// What to do is defined by the *what* parameter.
|
||||
///
|
||||
/// During execution of the job,
|
||||
/// While dc_imex() returns immediately, the started job may take a while,
|
||||
/// you can stop it using dc_stop_ongoing_process(). During execution of the job,
|
||||
/// some events are sent out:
|
||||
///
|
||||
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
|
||||
@@ -65,48 +68,41 @@ pub enum ImexMode {
|
||||
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
|
||||
///
|
||||
/// Only one import-/export-progress can run at the same time.
|
||||
/// To cancel an import-/export-progress, drop the future returned by this function.
|
||||
pub async fn imex(
|
||||
context: &Context,
|
||||
what: ImexMode,
|
||||
param1: Option<impl AsRef<Path>>,
|
||||
) -> Result<()> {
|
||||
use futures::future::FutureExt;
|
||||
/// To cancel an import-/export-progress, use dc_stop_ongoing_process().
|
||||
pub fn imex(context: &Context, what: ImexMode, param1: Option<impl AsRef<Path>>) {
|
||||
let mut param = Params::new();
|
||||
param.set_int(Param::Cmd, what as i32);
|
||||
if let Some(param1) = param1 {
|
||||
param.set(Param::Arg, param1.as_ref().to_string_lossy());
|
||||
}
|
||||
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
let res = imex_inner(context, what, param1)
|
||||
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
res
|
||||
job_kill_action(context, Action::ImexImap);
|
||||
job_add(context, Action::ImexImap, 0, param, 0);
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
|
||||
pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
|
||||
let dir_name = dir_name.as_ref();
|
||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||
let dir_iter = std::fs::read_dir(dir_name)?;
|
||||
let mut newest_backup_time = 0;
|
||||
let mut newest_backup_path: Option<PathBuf> = None;
|
||||
while let Some(dirent) = dir_iter.next().await {
|
||||
let mut newest_backup_path: Option<std::path::PathBuf> = None;
|
||||
for dirent in dir_iter {
|
||||
if let Ok(dirent) = dirent {
|
||||
let path = dirent.path();
|
||||
let name = dirent.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let sql = Sql::new();
|
||||
if sql.open(context, &path, true).await {
|
||||
if sql.open(context, &path, true) {
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int(context, "backup_time")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_time = curr_backup_time;
|
||||
}
|
||||
info!(context, "backup_time of {} is {}", name, curr_backup_time);
|
||||
sql.close().await;
|
||||
sql.close(&context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,32 +113,28 @@ pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
let res = do_initiate_key_transfer(context)
|
||||
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
pub fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
|
||||
let res = do_initiate_key_transfer(context);
|
||||
context.free_ongoing();
|
||||
res
|
||||
}
|
||||
|
||||
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
let mut msg: Message;
|
||||
let setup_code = create_setup_code(context);
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
ensure!(!context.shall_stop_ongoing(), "canceled");
|
||||
let setup_file_content = render_setup_file(context, &setup_code)?;
|
||||
/* encrypting may also take a while ... */
|
||||
ensure!(!context.shall_stop_ongoing(), "canceled");
|
||||
let setup_file_blob = BlobObject::create(
|
||||
context,
|
||||
"autocrypt-setup-message.html",
|
||||
setup_file_content.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF).await?;
|
||||
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?;
|
||||
msg = Message::default();
|
||||
msg.viewtype = Viewtype::File;
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
@@ -155,11 +147,12 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
ForcePlaintext::NoAutocryptHeader as i32,
|
||||
);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
ensure!(!context.shall_stop_ongoing(), "canceled");
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg)?;
|
||||
info!(context, "Wait for setup message being sent ...",);
|
||||
while !context.shall_stop_ongoing().await {
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
if let Ok(msg) = Message::load_from_db(context, msg_id).await {
|
||||
while !context.shall_stop_ongoing() {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
if let Ok(msg) = Message::load_from_db(context, msg_id) {
|
||||
if msg.is_sent() {
|
||||
info!(context, "... setup message sent.",);
|
||||
break;
|
||||
@@ -177,18 +170,20 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
/// Renders HTML body of a setup file message.
|
||||
///
|
||||
/// The `passphrase` must be at least 2 characters long.
|
||||
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
ensure!(
|
||||
passphrase.len() >= 2,
|
||||
"Passphrase must be at least 2 chars long."
|
||||
);
|
||||
let private_key = SignedSecretKey::load_self(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
||||
let self_addr = e2ee::ensure_secret_key_exists(context)?;
|
||||
let private_key = Key::from_self_private(context, self_addr, &context.sql)
|
||||
.ok_or_else(|| format_err!("Failed to get private key."))?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
};
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes()).await?;
|
||||
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes())?;
|
||||
|
||||
let replacement = format!(
|
||||
concat!(
|
||||
@@ -200,8 +195,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await;
|
||||
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await;
|
||||
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject);
|
||||
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody);
|
||||
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
|
||||
Ok(format!(
|
||||
concat!(
|
||||
@@ -244,8 +239,8 @@ pub fn create_setup_code(_context: &Context) -> String {
|
||||
ret
|
||||
}
|
||||
|
||||
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
|
||||
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
|
||||
fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
|
||||
if !context.sql.get_raw_config_bool(context, "bcc_self") {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
// TODO: define this as a stockstring once the wording is settled.
|
||||
msg.text = Some(
|
||||
@@ -254,30 +249,26 @@ async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
|
||||
go to the settings and enable \"Send copy to self\"."
|
||||
.to_string(),
|
||||
);
|
||||
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
|
||||
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn continue_key_transfer(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
setup_code: &str,
|
||||
) -> Result<()> {
|
||||
pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> {
|
||||
ensure!(!msg_id.is_special(), "wrong id");
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let msg = Message::load_from_db(context, msg_id)?;
|
||||
ensure!(
|
||||
msg.is_setupmessage(),
|
||||
"Message is no Autocrypt Setup Message."
|
||||
);
|
||||
|
||||
if let Some(filename) = msg.get_file(context) {
|
||||
let file = dc_open_file_std(context, filename)?;
|
||||
let file = dc_open_file(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, file).await?;
|
||||
set_self_key(context, &armored_key, true, true).await?;
|
||||
maybe_add_bcc_self_device_msg(context).await?;
|
||||
let armored_key = decrypt_setup_file(context, &sc, file)?;
|
||||
set_self_key(context, &armored_key, true, true)?;
|
||||
maybe_add_bcc_self_device_msg(context)?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -285,15 +276,19 @@ pub async fn continue_key_transfer(
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_self_key(
|
||||
fn set_self_key(
|
||||
context: &Context,
|
||||
armored: &str,
|
||||
set_default: bool,
|
||||
prefer_encrypt_required: bool,
|
||||
) -> Result<()> {
|
||||
// try hard to only modify key-state
|
||||
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
|
||||
let public_key = private_key.split_public_key()?;
|
||||
let keys = Key::from_armored_string(armored, KeyType::Private)
|
||||
.and_then(|(k, h)| if k.verify() { Some((k, h)) } else { None })
|
||||
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
|
||||
|
||||
ensure!(keys.is_some(), "Not a valid private key");
|
||||
let (private_key, public_key, header) = keys.unwrap();
|
||||
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
|
||||
match preferencrypt.map(|s| s.as_str()) {
|
||||
Some(headerval) => {
|
||||
@@ -306,8 +301,7 @@ async fn set_self_key(
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
|
||||
.await?;
|
||||
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)?;
|
||||
}
|
||||
None => {
|
||||
if prefer_encrypt_required {
|
||||
@@ -316,13 +310,18 @@ async fn set_self_key(
|
||||
}
|
||||
};
|
||||
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr).await;
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr);
|
||||
ensure!(self_addr.is_some(), "Missing self addr");
|
||||
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
|
||||
|
||||
let (public, secret) = match (public_key, private_key) {
|
||||
(Key::Public(p), Key::Secret(s)) => (p, s),
|
||||
_ => bail!("wrong keys unpacked"),
|
||||
};
|
||||
let keypair = pgp::KeyPair {
|
||||
addr,
|
||||
public: public_key,
|
||||
secret: private_key,
|
||||
public,
|
||||
secret,
|
||||
};
|
||||
key::store_self_keypair(
|
||||
context,
|
||||
@@ -332,16 +331,16 @@ async fn set_self_key(
|
||||
} else {
|
||||
key::KeyPairUse::ReadOnly
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
|
||||
fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
|
||||
_context: &Context,
|
||||
passphrase: &str,
|
||||
file: T,
|
||||
) -> Result<String> {
|
||||
let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
|
||||
let plain_bytes = pgp::symm_decrypt(passphrase, file)?;
|
||||
let plain_text = std::string::String::from_utf8(plain_bytes)?;
|
||||
|
||||
Ok(plain_text)
|
||||
@@ -360,50 +359,52 @@ pub fn normalize_setup_code(s: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
async fn imex_inner(
|
||||
context: &Context,
|
||||
what: ImexMode,
|
||||
param: Option<impl AsRef<Path>>,
|
||||
) -> Result<()> {
|
||||
ensure!(param.is_some(), "No Import/export dir/file given.");
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
|
||||
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
|
||||
let what: Option<ImexMode> = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32);
|
||||
let param = job.param.get(Param::Arg).unwrap_or_default();
|
||||
|
||||
ensure!(!param.is_empty(), "No Import/export dir/file given.");
|
||||
info!(context, "Import/export process started.");
|
||||
context.emit_event(Event::ImexProgress(10));
|
||||
context.call_cb(Event::ImexProgress(10));
|
||||
|
||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||
|
||||
let path = param.unwrap();
|
||||
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
|
||||
ensure!(context.sql.is_open(), "Database not opened.");
|
||||
if what == Some(ImexMode::ExportBackup) || what == Some(ImexMode::ExportSelfKeys) {
|
||||
// before we export anything, make sure the private key exists
|
||||
if e2ee::ensure_secret_key_exists(context).await.is_err() {
|
||||
if e2ee::ensure_secret_key_exists(context).is_err() {
|
||||
context.free_ongoing();
|
||||
bail!("Cannot create private key or private key not available.");
|
||||
} else {
|
||||
dc_create_folder(context, &path).await?;
|
||||
dc_create_folder(context, ¶m)?;
|
||||
}
|
||||
}
|
||||
|
||||
let path = Path::new(param);
|
||||
let success = match what {
|
||||
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
|
||||
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
|
||||
ImexMode::ExportBackup => export_backup(context, path).await,
|
||||
ImexMode::ImportBackup => import_backup(context, path).await,
|
||||
Some(ImexMode::ExportSelfKeys) => export_self_keys(context, path),
|
||||
Some(ImexMode::ImportSelfKeys) => import_self_keys(context, path),
|
||||
Some(ImexMode::ExportBackup) => export_backup(context, path),
|
||||
Some(ImexMode::ImportBackup) => import_backup(context, path),
|
||||
None => {
|
||||
bail!("unknown IMEX type");
|
||||
}
|
||||
};
|
||||
|
||||
context.free_ongoing();
|
||||
match success {
|
||||
Ok(()) => {
|
||||
info!(context, "IMEX successfully completed");
|
||||
context.emit_event(Event::ImexProgress(1000));
|
||||
context.call_cb(Event::ImexProgress(1000));
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
context.emit_event(Event::ImexProgress(0));
|
||||
context.call_cb(Event::ImexProgress(0));
|
||||
bail!("IMEX FAILED to complete: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Import Backup
|
||||
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
||||
fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
@@ -412,93 +413,84 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!context.is_configured().await,
|
||||
!context.is_configured(),
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
context.sql.close().await;
|
||||
dc_delete_file(context, context.get_dbfile()).await;
|
||||
context.sql.close(&context);
|
||||
dc_delete_file(context, context.get_dbfile());
|
||||
ensure!(
|
||||
!context.get_dbfile().exists().await,
|
||||
!context.get_dbfile().exists(),
|
||||
"Cannot delete old database."
|
||||
);
|
||||
|
||||
ensure!(
|
||||
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()).await,
|
||||
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()),
|
||||
"could not copy file"
|
||||
);
|
||||
/* error already logged */
|
||||
/* re-open copied database file */
|
||||
ensure!(
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await,
|
||||
context.sql.open(&context, &context.get_dbfile(), false),
|
||||
"could not re-open db"
|
||||
);
|
||||
|
||||
delete_and_reset_all_device_msgs(&context).await?;
|
||||
delete_and_reset_all_device_msgs(&context)?;
|
||||
|
||||
let total_files_cnt = context
|
||||
.sql
|
||||
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||
.await
|
||||
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![])
|
||||
.unwrap_or_default() as usize;
|
||||
info!(
|
||||
context,
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
);
|
||||
|
||||
let files = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let blob: Vec<u8> = row.get(1)?;
|
||||
let res = context.sql.query_map(
|
||||
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
|
||||
params![],
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let blob: Vec<u8> = row.get(1)?;
|
||||
|
||||
Ok((name, blob))
|
||||
},
|
||||
|files| {
|
||||
files
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok((name, blob))
|
||||
},
|
||||
|files| {
|
||||
for (processed_files_cnt, file) in files.enumerate() {
|
||||
let (file_name, file_blob) = file?;
|
||||
if context.shall_stop_ongoing() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
|
||||
if permille < 10 {
|
||||
permille = 10
|
||||
}
|
||||
if permille > 990 {
|
||||
permille = 990
|
||||
}
|
||||
context.call_cb(Event::ImexProgress(permille));
|
||||
if file_blob.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut all_files_extracted = true;
|
||||
for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
}
|
||||
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
|
||||
if permille < 10 {
|
||||
permille = 10
|
||||
}
|
||||
if permille > 990 {
|
||||
permille = 990
|
||||
}
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
if file_blob.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob)?;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
);
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
}
|
||||
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
match res {
|
||||
Ok(all_files_extracted) => {
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
sql::execute(context, &context.sql, "DROP TABLE backup_blobs;", params![])?;
|
||||
sql::try_execute(context, &context.sql, "VACUUM;").ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,31 +499,28 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
******************************************************************************/
|
||||
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
|
||||
The macro avoids weird values of 0% or 100% while still working. */
|
||||
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
|
||||
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
|
||||
let now = time();
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now)?;
|
||||
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
|
||||
|
||||
sql::housekeeping(context).await;
|
||||
sql::housekeeping(context);
|
||||
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
sql::try_execute(context, &context.sql, "VACUUM;").ok();
|
||||
|
||||
// we close the database during the copy of the dbfile
|
||||
context.sql.close().await;
|
||||
context.sql.close(context);
|
||||
info!(
|
||||
context,
|
||||
"Backup '{}' to '{}'.",
|
||||
context.get_dbfile().display(),
|
||||
dest_path_filename.display(),
|
||||
);
|
||||
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename).await;
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await;
|
||||
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename);
|
||||
context.sql.open(&context, &context.get_dbfile(), false);
|
||||
|
||||
if !copied {
|
||||
bail!(
|
||||
@@ -542,90 +531,86 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
}
|
||||
let dest_sql = Sql::new();
|
||||
ensure!(
|
||||
dest_sql.open(context, &dest_path_filename, false).await,
|
||||
dest_sql.open(context, &dest_path_filename, false),
|
||||
"could not open exported database {}",
|
||||
dest_path_string
|
||||
);
|
||||
let res = match add_files_to_export(context, &dest_sql).await {
|
||||
let res = match add_files_to_export(context, &dest_sql) {
|
||||
Err(err) => {
|
||||
dc_delete_file(context, &dest_path_filename).await;
|
||||
dc_delete_file(context, &dest_path_filename);
|
||||
error!(context, "backup failed: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
Ok(()) => {
|
||||
dest_sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
.await?;
|
||||
context.emit_event(Event::ImexFileWritten(dest_path_filename));
|
||||
dest_sql.set_raw_config_int(context, "backup_time", now as i32)?;
|
||||
context.call_cb(Event::ImexFileWritten(dest_path_filename));
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
dest_sql.close().await;
|
||||
dest_sql.close(context);
|
||||
|
||||
Ok(res?)
|
||||
}
|
||||
|
||||
async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
// add all files as blobs to the database copy (this does not require
|
||||
// the source to be locked, neigher the destination as it is used only here)
|
||||
if !sql.table_exists("backup_blobs").await? {
|
||||
sql.execute(
|
||||
if !sql.table_exists("backup_blobs") {
|
||||
sql::execute(
|
||||
context,
|
||||
&sql,
|
||||
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
params![],
|
||||
)?
|
||||
}
|
||||
// copy all files from BLOBDIR into backup-db
|
||||
let mut total_files_cnt = 0;
|
||||
let dir = context.get_blobdir();
|
||||
let dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count().await;
|
||||
let dir_handle = std::fs::read_dir(&dir)?;
|
||||
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count();
|
||||
|
||||
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
|
||||
// scan directory, pass 2: copy files
|
||||
let dir_handle = std::fs::read_dir(&dir)?;
|
||||
let exported_all_files = sql.prepare(
|
||||
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|
||||
|mut stmt, _| {
|
||||
let mut processed_files_cnt = 0;
|
||||
for entry in dir_handle {
|
||||
let entry = entry?;
|
||||
if context.shall_stop_ongoing() {
|
||||
return Ok(false);
|
||||
}
|
||||
processed_files_cnt += 1;
|
||||
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
|
||||
context.call_cb(Event::ImexProgress(permille));
|
||||
|
||||
sql.with_conn_async(|conn| async move {
|
||||
// scan directory, pass 2: copy files
|
||||
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||
|
||||
let mut processed_files_cnt = 0;
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let entry = entry?;
|
||||
if context.shall_stop_ongoing().await {
|
||||
return Ok(());
|
||||
}
|
||||
processed_files_cnt += 1;
|
||||
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
continue;
|
||||
}
|
||||
info!(context, "EXPORT: copying filename={}", name);
|
||||
let curr_path_filename = context.get_blobdir().join(entry.file_name());
|
||||
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
|
||||
if buf.is_empty() {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
continue;
|
||||
}
|
||||
// bail out if we can't insert
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|
||||
)?;
|
||||
stmt.execute(paramsv![name, buf])?;
|
||||
info!(context, "EXPORT: copying filename={}", name);
|
||||
let curr_path_filename = context.get_blobdir().join(entry.file_name());
|
||||
if let Ok(buf) = dc_read_file(context, &curr_path_filename) {
|
||||
if buf.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// bail out if we can't insert
|
||||
stmt.execute(params![name, buf])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)?;
|
||||
ensure!(exported_all_files, "canceled during export-files");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Classic key import
|
||||
******************************************************************************/
|
||||
async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
|
||||
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
|
||||
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
|
||||
@@ -636,8 +621,8 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
let mut imported_cnt = 0;
|
||||
|
||||
let dir_name = dir.as_ref().to_string_lossy();
|
||||
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let dir_handle = std::fs::read_dir(&dir)?;
|
||||
for entry in dir_handle {
|
||||
let entry_fn = entry?.file_name();
|
||||
let name_f = entry_fn.to_string_lossy();
|
||||
let path_plus_name = dir.as_ref().join(&entry_fn);
|
||||
@@ -657,10 +642,10 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match dc_read_file(context, &path_plus_name).await {
|
||||
match dc_read_file(context, &path_plus_name) {
|
||||
Ok(buf) => {
|
||||
let armored = std::string::String::from_utf8_lossy(&buf);
|
||||
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
|
||||
if let Err(err) = set_self_key(context, &armored, set_default, false) {
|
||||
error!(context, "set_self_key: {}", err);
|
||||
continue;
|
||||
}
|
||||
@@ -677,54 +662,45 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let mut export_errors = 0;
|
||||
|
||||
let keys = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let public_key_blob: Vec<u8> = row.get(1)?;
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||
let is_default: i32 = row.get(3)?;
|
||||
context.sql.query_map(
|
||||
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
||||
params![],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let public_key_blob: Vec<u8> = row.get(1)?;
|
||||
let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
|
||||
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
|
||||
let is_default: i32 = row.get(3)?;
|
||||
|
||||
Ok((id, public_key, private_key, is_default))
|
||||
},
|
||||
|keys| {
|
||||
keys.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok((id, public_key, private_key, is_default))
|
||||
},
|
||||
|keys| {
|
||||
for key_pair in keys {
|
||||
let (id, public_key, private_key, is_default) = key_pair?;
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
if let Some(key) = public_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
export_errors += 1;
|
||||
}
|
||||
if let Some(key) = private_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
export_errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (id, public_key, private_key, is_default) in keys {
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
if let Ok(key) = public_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
export_errors += 1;
|
||||
}
|
||||
if let Ok(key) = private_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
export_errors += 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
ensure!(export_errors == 0, "errors while exporting keys");
|
||||
Ok(())
|
||||
@@ -733,36 +709,26 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
/*******************************************************************************
|
||||
* Classic key export
|
||||
******************************************************************************/
|
||||
async fn export_key_to_asc_file<T>(
|
||||
fn export_key_to_asc_file(
|
||||
context: &Context,
|
||||
dir: impl AsRef<Path>,
|
||||
id: Option<i64>,
|
||||
key: &T,
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
T: DcKey + Any,
|
||||
{
|
||||
key: &Key,
|
||||
) -> std::io::Result<()> {
|
||||
let file_name = {
|
||||
let any_key = key as &dyn Any;
|
||||
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
|
||||
"public"
|
||||
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
|
||||
"private"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
let kind = if key.is_public() { "public" } else { "private" };
|
||||
let id = id.map_or("default".into(), |i| i.to_string());
|
||||
|
||||
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
|
||||
};
|
||||
info!(context, "Exporting key {}", file_name.display());
|
||||
dc_delete_file(context, &file_name).await;
|
||||
dc_delete_file(context, &file_name);
|
||||
|
||||
let content = key.to_asc(None).into_bytes();
|
||||
let res = dc_write_file(context, &file_name, &content).await;
|
||||
let res = key.write_asc_to_file(&file_name, context);
|
||||
if res.is_err() {
|
||||
error!(context, "Cannot write key to {}", file_name.display());
|
||||
} else {
|
||||
context.emit_event(Event::ImexFileWritten(file_name));
|
||||
context.call_cb(Event::ImexFileWritten(file_name));
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -774,12 +740,12 @@ mod tests {
|
||||
use crate::test_utils::*;
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file() {
|
||||
let t = test_context().await;
|
||||
#[test]
|
||||
fn test_render_setup_file() {
|
||||
let t = test_context(Some(Box::new(logging_cb)));
|
||||
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
|
||||
configure_alice_keypair(&t.ctx);
|
||||
let msg = render_setup_file(&t.ctx, "hello").unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
// In particular note the mixing of `\r\n` and `\n` depending
|
||||
@@ -790,25 +756,25 @@ mod tests {
|
||||
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
|
||||
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
|
||||
assert!(msg.contains("Passphrase-Begin: he\n"));
|
||||
assert!(msg.contains("==\n"));
|
||||
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_render_setup_file_newline_replace() {
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
|
||||
configure_alice_keypair(&t.ctx);
|
||||
let msg = render_setup_file(&t.ctx, "pw").unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_setup_code() {
|
||||
let t = dummy_context().await;
|
||||
#[test]
|
||||
fn test_create_setup_code() {
|
||||
let t = dummy_context();
|
||||
let setupcode = create_setup_code(&t.ctx);
|
||||
assert_eq!(setupcode.len(), 44);
|
||||
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
|
||||
@@ -821,17 +787,15 @@ mod tests {
|
||||
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_key_to_asc_file() {
|
||||
let context = dummy_context().await;
|
||||
let key = alice_keypair().public;
|
||||
#[test]
|
||||
fn test_export_key_to_asc_file() {
|
||||
let context = dummy_context();
|
||||
let key = Key::from(alice_keypair().public);
|
||||
let blobdir = "$BLOBDIR";
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
|
||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
||||
let filename = format!("{}/public-key-default.asc", blobdir);
|
||||
let bytes = async_std::fs::read(&filename).await.unwrap();
|
||||
let bytes = std::fs::read(&filename).unwrap();
|
||||
|
||||
assert_eq!(bytes, key.to_asc(None).into_bytes());
|
||||
}
|
||||
@@ -851,8 +815,11 @@ mod tests {
|
||||
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
|
||||
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_split_and_decrypt() {
|
||||
#[test]
|
||||
fn test_split_and_decrypt() {
|
||||
let ctx = dummy_context();
|
||||
let context = &ctx.ctx;
|
||||
|
||||
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
|
||||
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
|
||||
assert_eq!(typ, BlockType::Message);
|
||||
@@ -862,10 +829,12 @@ mod tests {
|
||||
assert!(!base64.is_empty());
|
||||
|
||||
let setup_file = S_EM_SETUPFILE.to_string();
|
||||
let decrypted =
|
||||
decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
let decrypted = decrypt_setup_file(
|
||||
context,
|
||||
S_EM_SETUPCODE,
|
||||
std::io::Cursor::new(setup_file.as_bytes()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
|
||||
|
||||
|
||||
1417
src/job.rs
1417
src/job.rs
File diff suppressed because it is too large
Load Diff
207
src/job_thread.rs
Normal file
207
src/job_thread.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::imap::Imap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JobThread {
|
||||
pub name: &'static str,
|
||||
pub folder_config_name: &'static str,
|
||||
pub imap: Imap,
|
||||
pub state: Arc<(Mutex<JobState>, Condvar)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct JobState {
|
||||
idle: bool,
|
||||
jobs_needed: bool,
|
||||
suspended: bool,
|
||||
using_handle: bool,
|
||||
}
|
||||
|
||||
impl JobThread {
|
||||
pub fn new(name: &'static str, folder_config_name: &'static str, imap: Imap) -> Self {
|
||||
JobThread {
|
||||
name,
|
||||
folder_config_name,
|
||||
imap,
|
||||
state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suspend(&self, context: &Context) {
|
||||
info!(context, "Suspending {}-thread.", self.name,);
|
||||
{
|
||||
self.state.0.lock().unwrap().suspended = true;
|
||||
}
|
||||
self.interrupt_idle(context);
|
||||
loop {
|
||||
let using_handle = self.state.0.lock().unwrap().using_handle;
|
||||
if !using_handle {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_micros(300 * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsuspend(&self, context: &Context) {
|
||||
info!(context, "Unsuspending {}-thread.", self.name);
|
||||
|
||||
let &(ref lock, ref cvar) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
state.suspended = false;
|
||||
state.idle = true;
|
||||
cvar.notify_one();
|
||||
}
|
||||
|
||||
pub fn interrupt_idle(&self, context: &Context) {
|
||||
{
|
||||
self.state.0.lock().unwrap().jobs_needed = true;
|
||||
}
|
||||
|
||||
info!(context, "Interrupting {}-IDLE...", self.name);
|
||||
|
||||
self.imap.interrupt_idle(context);
|
||||
|
||||
let &(ref lock, ref cvar) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
state.idle = true;
|
||||
cvar.notify_one();
|
||||
info!(context, "Interrupting {}-IDLE... finished", self.name);
|
||||
}
|
||||
|
||||
pub async fn fetch(&mut self, context: &Context, use_network: bool) {
|
||||
{
|
||||
let &(ref lock, _) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
if state.suspended {
|
||||
return;
|
||||
}
|
||||
|
||||
state.using_handle = true;
|
||||
}
|
||||
|
||||
if use_network {
|
||||
if let Err(err) = self.connect_and_fetch(context).await {
|
||||
warn!(context, "connect+fetch failed: {}, reconnect & retry", err);
|
||||
self.imap.trigger_reconnect();
|
||||
if let Err(err) = self.connect_and_fetch(context).await {
|
||||
warn!(context, "connect+fetch failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state.0.lock().unwrap().using_handle = false;
|
||||
}
|
||||
|
||||
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
|
||||
let prefix = format!("{}-fetch", self.name);
|
||||
match self.imap.connect_configured(context) {
|
||||
Ok(()) => {
|
||||
if let Some(watch_folder) = self.get_watch_folder(context) {
|
||||
let start = std::time::Instant::now();
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self
|
||||
.imap
|
||||
.fetch(context, &watch_folder)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
|
||||
|
||||
res
|
||||
} else {
|
||||
Err(Error::WatchFolderNotFound("not-set".to_string()))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(crate::error::Error::Message(err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_watch_folder(&self, context: &Context) -> Option<String> {
|
||||
match context.sql.get_raw_config(context, self.folder_config_name) {
|
||||
Some(name) => Some(name),
|
||||
None => {
|
||||
if self.folder_config_name == "configured_inbox_folder" {
|
||||
// initialized with old version, so has not set configured_inbox_folder
|
||||
Some("INBOX".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn idle(&self, context: &Context, use_network: bool) {
|
||||
{
|
||||
let &(ref lock, ref cvar) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
if state.jobs_needed {
|
||||
info!(
|
||||
context,
|
||||
"{}-IDLE will not be started as it was interrupted while not idling.",
|
||||
self.name,
|
||||
);
|
||||
state.jobs_needed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if state.suspended {
|
||||
while !state.idle {
|
||||
state = cvar.wait(state).unwrap();
|
||||
}
|
||||
state.idle = false;
|
||||
return;
|
||||
}
|
||||
|
||||
state.using_handle = true;
|
||||
|
||||
if !use_network {
|
||||
state.using_handle = false;
|
||||
|
||||
while !state.idle {
|
||||
state = cvar.wait(state).unwrap();
|
||||
}
|
||||
state.idle = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = format!("{}-IDLE", self.name);
|
||||
let do_fake_idle = match self.imap.connect_configured(context) {
|
||||
Ok(()) => {
|
||||
if !self.imap.can_idle() {
|
||||
true // we have to do fake_idle
|
||||
} else {
|
||||
let watch_folder = self.get_watch_folder(context);
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self.imap.idle(context, watch_folder);
|
||||
info!(context, "{} ended...", prefix);
|
||||
if let Err(err) = res {
|
||||
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
|
||||
// something is borked, let's start afresh on the next occassion
|
||||
self.imap.disconnect(context);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
info!(context, "{}-IDLE connection fail: {:?}", self.name, err);
|
||||
// if the connection fails, use fake_idle to retry periodically
|
||||
// fake_idle() will be woken up by interrupt_idle() as
|
||||
// well so will act on maybe_network events
|
||||
true
|
||||
}
|
||||
};
|
||||
if do_fake_idle {
|
||||
let watch_folder = self.get_watch_folder(context);
|
||||
self.imap.fake_idle(context, watch_folder);
|
||||
}
|
||||
|
||||
self.state.0.lock().unwrap().using_handle = false;
|
||||
}
|
||||
}
|
||||
765
src/key.rs
765
src/key.rs
@@ -1,46 +1,41 @@
|
||||
//! Cryptographic key module
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
|
||||
use crate::sql;
|
||||
use crate::dc_tools::*;
|
||||
use crate::sql::Sql;
|
||||
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
|
||||
/// Error type for deltachat key handling.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Could not decode base64")]
|
||||
Base64Decode(#[from] base64::DecodeError),
|
||||
#[error("rPGP error: {}", _0)]
|
||||
Pgp(#[from] pgp::errors::Error),
|
||||
#[error("Failed to generate PGP key: {}", _0)]
|
||||
Keygen(#[from] crate::pgp::PgpKeygenError),
|
||||
#[error("Failed to load key: {}", _0)]
|
||||
LoadKey(#[from] sql::Error),
|
||||
#[error("Failed to save generated key: {}", _0)]
|
||||
StoreKey(#[from] SaveKeyError),
|
||||
#[error("No address configured")]
|
||||
NoConfiguredAddr,
|
||||
#[error("Configured address is invalid: {}", _0)]
|
||||
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||
#[error("no data provided")]
|
||||
Empty,
|
||||
#[fail(display = "Could not decode base64")]
|
||||
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
|
||||
#[fail(display = "rPGP error: {}", _0)]
|
||||
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Error {
|
||||
Error::Base64Decode(err, failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::errors::Error> for Error {
|
||||
fn from(err: pgp::errors::Error) -> Error {
|
||||
Error::PgpError(err, failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -50,9 +45,8 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
/// 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;
|
||||
pub trait DcKey: Serialize + Deserializable {
|
||||
type KeyType: Serialize + Deserializable;
|
||||
|
||||
/// Create a key from some bytes.
|
||||
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
|
||||
@@ -69,200 +63,243 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
Self::from_slice(&bytes)
|
||||
}
|
||||
|
||||
/// Create a key from an ASCII-armored string.
|
||||
///
|
||||
/// Returns the key and a map of any headers which might have been set in
|
||||
/// the ASCII-armored representation.
|
||||
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
|
||||
let bytes = data.as_bytes();
|
||||
Self::KeyType::from_armor_single(Cursor::new(bytes)).map_err(Error::Pgp)
|
||||
}
|
||||
|
||||
/// Load the users' default key from the database.
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType>;
|
||||
|
||||
/// Serialise the key as bytes.
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
/// Serialise the key to a base64 string.
|
||||
fn to_base64(&self) -> String {
|
||||
// Not using Serialize::to_bytes() to make clear *why* it is
|
||||
// safe to ignore this error.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
/// Serialise the key to a base64 string.
|
||||
fn to_base64(&self) -> String {
|
||||
base64::encode(&DcKey::to_bytes(self))
|
||||
}
|
||||
|
||||
/// Serialise the key to ASCII-armored representation.
|
||||
///
|
||||
/// Each header line must be terminated by `\r\n`. Only allows setting one
|
||||
/// header as a simplification since that's the only way it's used so far.
|
||||
// Since .to_armored_string() are actual methods on SignedPublicKey and
|
||||
// SignedSecretKey we can not generically implement this.
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
|
||||
|
||||
/// The fingerprint for the key.
|
||||
fn fingerprint(&self) -> Fingerprint {
|
||||
Fingerprint::new(KeyTrait::fingerprint(self)).expect("Invalid fingerprint from rpgp")
|
||||
base64::encode(&buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
// Not using .to_armored_string() to make clear *why* it is
|
||||
// safe to ignore this error.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let headers = header.map(|(key, value)| {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert(key.to_string(), value.to_string());
|
||||
m
|
||||
});
|
||||
let mut buf = Vec::new();
|
||||
self.to_armored_writer(&mut buf, headers.as_ref())
|
||||
.unwrap_or_default();
|
||||
std::string::String::from_utf8(buf).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
}
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
/// Cryptographic key
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Key {
|
||||
Public(SignedPublicKey),
|
||||
Secret(SignedSecretKey),
|
||||
}
|
||||
|
||||
impl From<SignedPublicKey> for Key {
|
||||
fn from(key: SignedPublicKey) -> Self {
|
||||
Key::Public(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignedSecretKey> for Key {
|
||||
fn from(key: SignedSecretKey) -> Self {
|
||||
Key::Secret(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<Key> for SignedSecretKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(_) => Err(()),
|
||||
Key::Secret(key) => Ok(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(_) => Err(()),
|
||||
Key::Secret(key) => Ok(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<Key> for SignedPublicKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(key) => Ok(key),
|
||||
Key::Secret(_) => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(key) => Ok(key),
|
||||
Key::Secret(_) => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn is_public(&self) -> bool {
|
||||
match self {
|
||||
Key::Public(_) => true,
|
||||
Key::Secret(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
// Not using .to_armored_string() to make clear *why* it is
|
||||
// safe to do these unwraps.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error. The string is always ASCII.
|
||||
pub fn is_secret(&self) -> bool {
|
||||
!self.is_public()
|
||||
}
|
||||
|
||||
pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Option<Self> {
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let res: std::result::Result<Key, _> = match key_type {
|
||||
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
|
||||
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(key) => Some(key),
|
||||
Err(err) => {
|
||||
eprintln!("Invalid key bytes: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_armored_string(
|
||||
data: &str,
|
||||
key_type: KeyType,
|
||||
) -> Option<(Self, BTreeMap<String, String>)> {
|
||||
let bytes = data.as_bytes();
|
||||
let res: std::result::Result<(Key, _), _> = match key_type {
|
||||
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
|
||||
.map(|(k, h)| (Into::into(k), h)),
|
||||
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
|
||||
.map(|(k, h)| (Into::into(k), h)),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(res) => Some(res),
|
||||
Err(err) => {
|
||||
eprintln!("Invalid key bytes: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_self_public(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
sql: &Sql,
|
||||
) -> Option<Self> {
|
||||
let addr = self_addr.as_ref();
|
||||
|
||||
sql.query_get_value(
|
||||
context,
|
||||
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
|
||||
&[addr],
|
||||
)
|
||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
|
||||
}
|
||||
|
||||
pub fn from_self_private(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
sql: &Sql,
|
||||
) -> Option<Self> {
|
||||
sql.query_get_value(
|
||||
context,
|
||||
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
|
||||
&[self_addr.as_ref()],
|
||||
)
|
||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Key::Public(k) => k.to_bytes().unwrap_or_default(),
|
||||
Key::Secret(k) => k.to_bytes().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> bool {
|
||||
match self {
|
||||
Key::Public(k) => k.verify().is_ok(),
|
||||
Key::Secret(k) => k.verify().is_ok(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
let buf = self.to_bytes();
|
||||
base64::encode(&buf)
|
||||
}
|
||||
|
||||
pub fn to_armored_string(
|
||||
&self,
|
||||
headers: Option<&BTreeMap<String, String>>,
|
||||
) -> pgp::errors::Result<String> {
|
||||
match self {
|
||||
Key::Public(k) => k.to_armored_string(headers),
|
||||
Key::Secret(k) => k.to_armored_string(headers),
|
||||
}
|
||||
}
|
||||
|
||||
/// Each header line must be terminated by `\r\n`
|
||||
pub fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
let headers = header.map(|(key, value)| {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert(key.to_string(), value.to_string());
|
||||
m
|
||||
});
|
||||
let mut buf = Vec::new();
|
||||
self.to_armored_writer(&mut buf, headers.as_ref())
|
||||
.unwrap_or_default();
|
||||
std::string::String::from_utf8(buf).unwrap_or_default()
|
||||
|
||||
self.to_armored_string(headers.as_ref())
|
||||
.expect("failed to serialize key")
|
||||
}
|
||||
}
|
||||
|
||||
/// Deltachat extension trait for secret keys.
|
||||
///
|
||||
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
|
||||
pub trait DcSecretKey {
|
||||
/// Create a public key from a private one.
|
||||
fn split_public_key(&self) -> Result<SignedPublicKey>;
|
||||
}
|
||||
pub fn write_asc_to_file(
|
||||
&self,
|
||||
file: impl AsRef<Path>,
|
||||
context: &Context,
|
||||
) -> std::io::Result<()> {
|
||||
let file_content = self.to_asc(None).into_bytes();
|
||||
|
||||
impl DcSecretKey for SignedSecretKey {
|
||||
fn split_public_key(&self) -> Result<SignedPublicKey> {
|
||||
self.verify()?;
|
||||
let unsigned_pubkey = SecretKeyTrait::public_key(self);
|
||||
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
|
||||
Ok(signed_pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.ok_or_else(|| Error::NoConfiguredAddr)?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let start = std::time::Instant::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
let keypair =
|
||||
async_std::task::spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
|
||||
.await?;
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair)
|
||||
let res = dc_write_file(context, &file, &file_content);
|
||||
if res.is_err() {
|
||||
error!(context, "Cannot write key to {}", file.as_ref().display());
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> String {
|
||||
match self {
|
||||
Key::Public(k) => hex::encode_upper(k.fingerprint()),
|
||||
Key::Secret(k) => hex::encode_upper(k.fingerprint()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formatted_fingerprint(&self) -> String {
|
||||
let rawhex = self.fingerprint();
|
||||
dc_format_fingerprint(&rawhex)
|
||||
}
|
||||
|
||||
pub fn split_key(&self) -> Option<Key> {
|
||||
match self {
|
||||
Key::Public(_) => None,
|
||||
Key::Secret(k) => {
|
||||
let pub_key = k.public_key();
|
||||
pub_key.sign(k, || "".into()).map(Key::Public).ok()
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,19 +316,21 @@ pub enum KeyPairUse {
|
||||
}
|
||||
|
||||
/// Error saving a keypair to the database.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("SaveKeyError: {message}")]
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "SaveKeyError: {}", message)]
|
||||
pub struct SaveKeyError {
|
||||
message: String,
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
#[cause]
|
||||
cause: failure::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
}
|
||||
|
||||
impl SaveKeyError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,130 +347,103 @@ impl SaveKeyError {
|
||||
/// same key again overwrites it.
|
||||
///
|
||||
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
|
||||
pub async fn store_self_keypair(
|
||||
pub fn store_self_keypair(
|
||||
context: &Context,
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> std::result::Result<(), SaveKeyError> {
|
||||
// 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);
|
||||
let public_key = keypair
|
||||
.public
|
||||
.to_bytes()
|
||||
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
|
||||
let secret_key = keypair
|
||||
.secret
|
||||
.to_bytes()
|
||||
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
params![public_key, secret_key],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.execute("UPDATE keypairs SET is_default=0;", params![])
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
}
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true as i32,
|
||||
KeyPairUse::ReadOnly => false as i32,
|
||||
KeyPairUse::Default => true,
|
||||
KeyPairUse::ReadOnly => false,
|
||||
};
|
||||
|
||||
let addr = keypair.addr.to_string();
|
||||
let t = time();
|
||||
|
||||
let params = paramsv![addr, is_default, public_key, secret_key, t];
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
params,
|
||||
params![
|
||||
keypair.addr.to_string(),
|
||||
is_default as i32,
|
||||
public_key,
|
||||
secret_key,
|
||||
time()
|
||||
],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
|
||||
|
||||
Ok(())
|
||||
.map(|_| ())
|
||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
|
||||
}
|
||||
|
||||
/// A key fingerprint
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
/// Make a fingerprint human-readable, in hex format.
|
||||
pub fn dc_format_fingerprint(fingerprint: &str) -> String {
|
||||
// split key into chunks of 4 with space, and 20 newline
|
||||
let mut res = String::new();
|
||||
|
||||
impl Fingerprint {
|
||||
pub fn new(v: Vec<u8>) -> std::result::Result<Fingerprint, FingerprintError> {
|
||||
match v.len() {
|
||||
20 => Ok(Fingerprint(v)),
|
||||
_ => Err(FingerprintError::WrongLength),
|
||||
for (i, c) in fingerprint.chars().enumerate() {
|
||||
if i > 0 && i % 20 == 0 {
|
||||
res += "\n";
|
||||
} else if i > 0 && i % 4 == 0 {
|
||||
res += " ";
|
||||
}
|
||||
|
||||
res += &c.to_string();
|
||||
}
|
||||
|
||||
/// Make a hex string from the fingerprint.
|
||||
///
|
||||
/// Use [std::fmt::Display] or [ToString::to_string] to get a
|
||||
/// human-readable formatted string.
|
||||
pub fn hex(&self) -> String {
|
||||
hex::encode_upper(&self.0)
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Make a human-readable fingerprint.
|
||||
impl fmt::Display for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Split key into chunks of 4 with space and newline at 20 chars
|
||||
for (i, c) in self.hex().chars().enumerate() {
|
||||
if i > 0 && i % 20 == 0 {
|
||||
writeln!(f)?;
|
||||
} else if i > 0 && i % 4 == 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{}", c)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a human-readable or otherwise formatted fingerprint.
|
||||
impl std::str::FromStr for Fingerprint {
|
||||
type Err = FingerprintError;
|
||||
|
||||
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let hex_repr: String = input
|
||||
.to_uppercase()
|
||||
.chars()
|
||||
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(hex_repr)?;
|
||||
let fp = Fingerprint::new(v)?;
|
||||
Ok(fp)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FingerprintError {
|
||||
#[error("Invalid hex characters")]
|
||||
NotHex(#[from] hex::FromHexError),
|
||||
#[error("Incorrect fingerprint lengths")]
|
||||
WrongLength,
|
||||
/// Bring a human-readable or otherwise formatted fingerprint back to the 40-characters-uppercase-hex format.
|
||||
pub fn dc_normalize_fingerprint(fp: &str) -> String {
|
||||
fp.to_uppercase()
|
||||
.chars()
|
||||
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use async_std::sync::Arc;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref KEYPAIR: KeyPair = alice_keypair();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_fingerprint() {
|
||||
let fingerprint = dc_normalize_fingerprint(" 1234 567890 \n AbcD abcdef ABCDEF ");
|
||||
|
||||
assert_eq!(fingerprint, "1234567890ABCDABCDEFABCDEF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_armored_string() {
|
||||
let (private_key, _) = SignedSecretKey::from_asc(
|
||||
let (private_key, _) = Key::from_armored_string(
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
xcLYBF0fgz4BCADnRUV52V4xhSsU56ZaAn3+3oG86MZhXy4X8w14WZZDf0VJGeTh
|
||||
@@ -489,147 +501,99 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
7yPJeQ==
|
||||
=KZk/
|
||||
-----END PGP PRIVATE KEY BLOCK-----",
|
||||
KeyType::Private,
|
||||
)
|
||||
.expect("failed to decode");
|
||||
let binary = DcKey::to_bytes(&private_key);
|
||||
SignedSecretKey::from_slice(&binary).expect("invalid private key");
|
||||
.expect("failed to decode"); // NOTE: if you take out the ===GU1/ part, everything passes!
|
||||
let binary = private_key.to_bytes();
|
||||
Key::from_slice(&binary, KeyType::Private).expect("invalid private key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asc_roundtrip() {
|
||||
let key = KEYPAIR.public.clone();
|
||||
let asc = key.to_asc(Some(("spam", "ham")));
|
||||
let (key2, hdrs) = SignedPublicKey::from_asc(&asc).unwrap();
|
||||
assert_eq!(key, key2);
|
||||
assert_eq!(hdrs.len(), 1);
|
||||
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
|
||||
fn test_format_fingerprint() {
|
||||
let fingerprint = dc_format_fingerprint("1234567890ABCDABCDEFABCDEF1234567890ABCD");
|
||||
|
||||
let key = KEYPAIR.secret.clone();
|
||||
let asc = key.to_asc(Some(("spam", "ham")));
|
||||
let (key2, hdrs) = SignedSecretKey::from_asc(&asc).unwrap();
|
||||
assert_eq!(key, key2);
|
||||
assert_eq!(hdrs.len(), 1);
|
||||
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
|
||||
assert_eq!(
|
||||
fingerprint,
|
||||
"1234 5678 90AB CDAB CDEF\nABCD EF12 3456 7890 ABCD"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_roundtrip() {
|
||||
let public_key = KEYPAIR.public.clone();
|
||||
let private_key = KEYPAIR.secret.clone();
|
||||
let public_key = Key::from(KEYPAIR.public.clone());
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
|
||||
let binary = DcKey::to_bytes(&public_key);
|
||||
let public_key2 = SignedPublicKey::from_slice(&binary).expect("invalid public key");
|
||||
let binary = public_key.to_bytes();
|
||||
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
|
||||
assert_eq!(public_key, public_key2);
|
||||
|
||||
let binary = DcKey::to_bytes(&private_key);
|
||||
let private_key2 = SignedSecretKey::from_slice(&binary).expect("invalid private key");
|
||||
let binary = private_key.to_bytes();
|
||||
let private_key2 = Key::from_slice(&binary, KeyType::Private).expect("invalid private key");
|
||||
assert_eq!(private_key, private_key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_bad_data() {
|
||||
let mut bad_data: [u8; 4096] = [0; 4096];
|
||||
|
||||
for i in 0..4096 {
|
||||
bad_data[i] = (i & 0xff) as u8;
|
||||
}
|
||||
|
||||
for j in 0..(4096 / 40) {
|
||||
let slice = &bad_data[j..j + 4096 / 2 + j];
|
||||
assert!(SignedPublicKey::from_slice(slice).is_err());
|
||||
assert!(SignedSecretKey::from_slice(slice).is_err());
|
||||
let bad_key = Key::from_slice(
|
||||
&bad_data[j..j + 4096 / 2 + j],
|
||||
if 0 != j & 1 {
|
||||
KeyType::Public
|
||||
} else {
|
||||
KeyType::Private
|
||||
},
|
||||
);
|
||||
assert!(bad_key.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base64_roundtrip() {
|
||||
let key = KEYPAIR.public.clone();
|
||||
let base64 = key.to_base64();
|
||||
let key2 = SignedPublicKey::from_base64(&base64).unwrap();
|
||||
assert_eq!(key, key2);
|
||||
}
|
||||
fn test_ascii_roundtrip() {
|
||||
let public_key = Key::from(KEYPAIR.public.clone());
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = dummy_context().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
|
||||
assert_eq!(alice.secret, seckey);
|
||||
}
|
||||
let s = public_key.to_armored_string(None).unwrap();
|
||||
let (public_key2, _) =
|
||||
Key::from_armored_string(&s, KeyType::Public).expect("invalid public key");
|
||||
assert_eq!(public_key, public_key2);
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t.ctx).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t.ctx).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_concurrent() {
|
||||
use std::thread;
|
||||
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = t.ctx.clone();
|
||||
let ctx0 = ctx.clone();
|
||||
let thr0 =
|
||||
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
|
||||
let ctx1 = ctx.clone();
|
||||
let thr1 =
|
||||
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
|
||||
let res0 = thr0.join().unwrap();
|
||||
let res1 = thr1.join().unwrap();
|
||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
||||
let s = private_key.to_armored_string(None).unwrap();
|
||||
println!("{}", &s);
|
||||
let (private_key2, _) =
|
||||
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
|
||||
assert_eq!(private_key, private_key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_key() {
|
||||
let pubkey = KEYPAIR.secret.split_public_key().unwrap();
|
||||
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
let public_wrapped = private_key.split_key().unwrap();
|
||||
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
|
||||
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_save_self_key_twice() {
|
||||
#[test]
|
||||
fn test_save_self_key_twice() {
|
||||
// Saving the same key twice should result in only one row in
|
||||
// the keypairs table.
|
||||
let t = dummy_context().await;
|
||||
let ctx = Arc::new(t.ctx);
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let nrows = || async {
|
||||
ctx1.sql
|
||||
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await
|
||||
let t = dummy_context();
|
||||
let nrows = || {
|
||||
t.ctx
|
||||
.sql
|
||||
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(nrows().await, 0);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(nrows().await, 1);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(nrows().await, 1);
|
||||
assert_eq!(nrows(), 0);
|
||||
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
|
||||
assert_eq!(nrows(), 1);
|
||||
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
|
||||
assert_eq!(nrows(), 1);
|
||||
}
|
||||
|
||||
// Convenient way to create a new key if you need one, run with
|
||||
@@ -652,49 +616,4 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
let fp: Fingerprint = "zzzz 0102 0304 0506\n0708090a0b0c0D0E0F1011121314 yyy"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(fp, res);
|
||||
|
||||
let err = "1".parse::<Fingerprint>().err().unwrap();
|
||||
match err {
|
||||
FingerprintError::NotHex(_) => (),
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
let src_err = err.source().unwrap().downcast_ref::<hex::FromHexError>();
|
||||
assert_eq!(src_err, Some(&hex::FromHexError::OddLength));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
103
src/keyring.rs
103
src/keyring.rs
@@ -1,92 +1,45 @@
|
||||
//! Keyring to perform rpgp operations with.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::constants::KeyType;
|
||||
use crate::context::Context;
|
||||
use crate::key::{self, DcKey};
|
||||
use crate::key::Key;
|
||||
use crate::sql::Sql;
|
||||
|
||||
/// An in-memory keyring.
|
||||
///
|
||||
/// Instances are usually constructed just for the rpgp operation and
|
||||
/// short-lived.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Keyring<T>
|
||||
where
|
||||
T: DcKey,
|
||||
{
|
||||
keys: Vec<T>,
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Keyring<'a> {
|
||||
keys: Vec<Cow<'a, Key>>,
|
||||
}
|
||||
|
||||
impl<T> Keyring<T>
|
||||
where
|
||||
T: DcKey<KeyType = T>,
|
||||
{
|
||||
/// New empty keyring.
|
||||
pub fn new() -> Keyring<T> {
|
||||
Keyring { keys: Vec::new() }
|
||||
impl<'a> Keyring<'a> {
|
||||
pub fn add_owned(&mut self, key: Key) {
|
||||
self.add(Cow::Owned(key))
|
||||
}
|
||||
|
||||
/// Create a new keyring with the the user's secret key loaded.
|
||||
pub async fn new_self(context: &Context) -> Result<Keyring<T>, key::Error> {
|
||||
let mut keyring: Keyring<T> = Keyring::new();
|
||||
keyring.load_self(context).await?;
|
||||
Ok(keyring)
|
||||
pub fn add_ref(&mut self, key: &'a Key) {
|
||||
self.add(Cow::Borrowed(key))
|
||||
}
|
||||
|
||||
/// Load the user's key into the keyring.
|
||||
pub async fn load_self(&mut self, context: &Context) -> Result<(), key::Error> {
|
||||
self.add(T::load_self(context).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a key to the keyring.
|
||||
pub fn add(&mut self, key: T) {
|
||||
fn add(&mut self, key: Cow<'a, Key>) {
|
||||
self.keys.push(key);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
/// A vector with reference to all the keys in the keyring.
|
||||
pub fn keys(&self) -> &[T] {
|
||||
pub fn keys(&self) -> &[Cow<'a, Key>] {
|
||||
&self.keys
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::key::{SignedPublicKey, SignedSecretKey};
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
fn test_keyring_add_keys() {
|
||||
let alice = alice_keypair();
|
||||
let mut pub_ring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
pub_ring.add(alice.public.clone());
|
||||
assert_eq!(pub_ring.keys(), [alice.public]);
|
||||
|
||||
let mut sec_ring: Keyring<SignedSecretKey> = Keyring::new();
|
||||
sec_ring.add(alice.secret.clone());
|
||||
assert_eq!(sec_ring.keys(), [alice.secret]);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_keyring_load_self() {
|
||||
// new_self() implies load_self()
|
||||
let t = dummy_context().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let alice = alice_keypair();
|
||||
|
||||
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t.ctx).await.unwrap();
|
||||
assert_eq!(pub_ring.keys(), [alice.public]);
|
||||
|
||||
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t.ctx).await.unwrap();
|
||||
assert_eq!(sec_ring.keys(), [alice.secret]);
|
||||
pub fn load_self_private_for_decrypting(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
sql: &Sql,
|
||||
) -> bool {
|
||||
sql.query_get_value(
|
||||
context,
|
||||
"SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;",
|
||||
&[self_addr.as_ref()],
|
||||
)
|
||||
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Private))
|
||||
.map(|key| self.add_owned(key))
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
19
src/lib.rs
19
src/lib.rs
@@ -2,6 +2,8 @@
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![allow(clippy::match_bool)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
#[macro_use]
|
||||
@@ -11,23 +13,14 @@ extern crate rusqlite;
|
||||
extern crate strum;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
#[macro_use]
|
||||
extern crate debug_stub_derive;
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
#[macro_use]
|
||||
pub mod sql;
|
||||
#[cfg(not(feature = "internals"))]
|
||||
#[macro_use]
|
||||
mod sql;
|
||||
|
||||
pub mod headerdef;
|
||||
|
||||
pub(crate) mod events;
|
||||
@@ -45,9 +38,9 @@ pub mod context;
|
||||
mod e2ee;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
mod scheduler;
|
||||
#[macro_use]
|
||||
pub mod job;
|
||||
mod job_thread;
|
||||
pub mod key;
|
||||
mod keyring;
|
||||
pub mod location;
|
||||
@@ -65,9 +58,9 @@ pub mod qr;
|
||||
pub mod securejoin;
|
||||
mod simplify;
|
||||
mod smtp;
|
||||
pub mod sql;
|
||||
pub mod stock;
|
||||
mod token;
|
||||
pub(crate) mod upload;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
|
||||
|
||||
442
src/location.rs
442
src/location.rs
@@ -1,6 +1,7 @@
|
||||
//! Location handling
|
||||
|
||||
use bitflags::bitflags;
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
@@ -8,12 +9,13 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
use crate::job::{self, Job};
|
||||
use crate::job::{self, job_action_exists, job_add, Job};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// Location record
|
||||
@@ -121,9 +123,9 @@ impl Kml {
|
||||
}
|
||||
} else if self.tag.contains(KmlTag::COORDINATES) {
|
||||
let parts = val.splitn(2, ',').collect::<Vec<_>>();
|
||||
if let [longitude, latitude] = &parts[..] {
|
||||
self.curr.longitude = longitude.parse().unwrap_or_default();
|
||||
self.curr.latitude = latitude.parse().unwrap_or_default();
|
||||
if parts.len() == 2 {
|
||||
self.curr.longitude = parts[0].parse().unwrap_or_default();
|
||||
self.curr.latitude = parts[1].parse().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,103 +192,91 @@ impl Kml {
|
||||
}
|
||||
|
||||
// location streaming
|
||||
pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
|
||||
pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
|
||||
let now = time();
|
||||
if !(seconds < 0 || chat_id.is_special()) {
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id).await;
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
|
||||
if sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
params![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(
|
||||
context
|
||||
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
|
||||
.await,
|
||||
);
|
||||
msg.text =
|
||||
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
|
||||
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = context
|
||||
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, stock_str).await;
|
||||
let stock_str =
|
||||
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
|
||||
chat::add_info_msg(context, chat_id, stock_str);
|
||||
}
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
job::add(
|
||||
schedule_MAYBE_SEND_LOCATIONS(context, false);
|
||||
job_add(
|
||||
context,
|
||||
job::Job::new(
|
||||
job::Action::MaybeSendLocationsEnded,
|
||||
chat_id.to_u32(),
|
||||
Params::new(),
|
||||
seconds + 1,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
job::Action::MaybeSendLocationsEnded,
|
||||
chat_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
seconds + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) {
|
||||
if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await {
|
||||
job::add(
|
||||
#[allow(non_snake_case)]
|
||||
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) {
|
||||
if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) {
|
||||
job_add(
|
||||
context,
|
||||
job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60),
|
||||
)
|
||||
.await;
|
||||
job::Action::MaybeSendLocations,
|
||||
0,
|
||||
Params::new(),
|
||||
60,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
|
||||
pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
|
||||
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return true;
|
||||
}
|
||||
let mut continue_streaming = false;
|
||||
|
||||
if let Ok(chats) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Ok(chats) = context.sql.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
params![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
) {
|
||||
for chat_id in chats {
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
params![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
@@ -294,22 +284,22 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
]
|
||||
).await {
|
||||
) {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
continue_streaming = true;
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
schedule_MAYBE_SEND_LOCATIONS(context, false);
|
||||
}
|
||||
|
||||
continue_streaming
|
||||
}
|
||||
|
||||
pub async fn get_range(
|
||||
pub fn get_range(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
@@ -328,7 +318,7 @@ pub async fn get_range(
|
||||
AND (? OR l.from_id=?) \
|
||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
||||
paramsv![
|
||||
params![
|
||||
if chat_id.is_unset() { 1 } else { 0 },
|
||||
chat_id,
|
||||
if contact_id == 0 { 1 } else { 0 },
|
||||
@@ -367,7 +357,6 @@ pub async fn get_range(
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -376,33 +365,28 @@ fn is_marker(txt: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Deletes all locations from the database.
|
||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
.await?;
|
||||
context.emit_event(Event::LocationChanged(None));
|
||||
pub fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
|
||||
context.call_cb(Event::LocationChanged(None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
|
||||
pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||
paramsv![chat_id], |row| {
|
||||
params![chat_id], |row| {
|
||||
let send_begin: i64 = row.get(0)?;
|
||||
let send_until: i64 = row.get(1)?;
|
||||
let last_sent: i64 = row.get(2)?;
|
||||
|
||||
Ok((send_begin, send_until, last_sent))
|
||||
})
|
||||
.await?;
|
||||
})?;
|
||||
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
@@ -421,7 +405,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
AND independent=0 \
|
||||
GROUP BY timestamp \
|
||||
ORDER BY timestamp;",
|
||||
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|
||||
params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|
||||
|row| {
|
||||
let location_id: i32 = row.get(0)?;
|
||||
let latitude: f64 = row.get(1)?;
|
||||
@@ -446,7 +430,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
)?;
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
@@ -479,38 +463,37 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn set_kml_sent_timestamp(
|
||||
pub fn set_kml_sent_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
paramsv![timestamp, chat_id],
|
||||
)
|
||||
.await?;
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
params![timestamp, chat_id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_msg_location_id(
|
||||
pub fn set_msg_location_id(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
location_id: u32,
|
||||
) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
paramsv![location_id, msg_id],
|
||||
)
|
||||
.await?;
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
params![location_id, msg_id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save(
|
||||
pub fn save(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
@@ -518,66 +501,54 @@ pub async fn save(
|
||||
independent: bool,
|
||||
) -> Result<u32, Error> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let mut newest_timestamp = 0;
|
||||
let mut newest_location_id = 0;
|
||||
|
||||
for location in locations {
|
||||
let &Location {
|
||||
timestamp,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
..
|
||||
} = location;
|
||||
context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let mut stmt_test = conn
|
||||
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||
let mut stmt_insert = conn.prepare_cached(
|
||||
"INSERT INTO locations\
|
||||
context
|
||||
.sql
|
||||
.prepare2(
|
||||
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
|
||||
"INSERT INTO locations\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
VALUES (?,?,?,?,?,?,?);",
|
||||
)?;
|
||||
|mut stmt_test, mut stmt_insert, conn| {
|
||||
let mut newest_timestamp = 0;
|
||||
let mut newest_location_id = 0;
|
||||
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||
for location in locations {
|
||||
let exists =
|
||||
stmt_test.exists(params![location.timestamp, contact_id as i32])?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
timestamp,
|
||||
contact_id as i32,
|
||||
chat_id,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
independent,
|
||||
])?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = crate::sql::get_rowid2(
|
||||
&mut conn,
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(params![
|
||||
location.timestamp,
|
||||
contact_id as i32,
|
||||
)?;
|
||||
chat_id,
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
location.accuracy,
|
||||
independent,
|
||||
])?;
|
||||
|
||||
if location.timestamp > newest_timestamp {
|
||||
newest_timestamp = location.timestamp;
|
||||
newest_location_id = sql::get_rowid2_with_conn(
|
||||
context,
|
||||
conn,
|
||||
"locations",
|
||||
"timestamp",
|
||||
location.timestamp,
|
||||
"from_id",
|
||||
contact_id as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
Ok(newest_location_id)
|
||||
},
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
|
||||
let now = time();
|
||||
let mut continue_streaming = false;
|
||||
info!(
|
||||
@@ -585,118 +556,101 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
|
||||
);
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
if let Ok(rows) = context.sql.query_map(
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
FROM chats \
|
||||
WHERE locations_send_until>?;",
|
||||
paramsv![now],
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let locations_send_begin: i64 = row.get(1)?;
|
||||
let locations_last_sent: i64 = row.get(2)?;
|
||||
continue_streaming = true;
|
||||
params![now],
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let locations_send_begin: i64 = row.get(1)?;
|
||||
let locations_last_sent: i64 = row.get(2)?;
|
||||
continue_streaming = true;
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if rows.is_ok() {
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
) {
|
||||
let msgs = context
|
||||
.sql
|
||||
.with_conn(move |conn| {
|
||||
let rows = rows.unwrap();
|
||||
|
||||
let mut stmt_locations = conn.prepare_cached(
|
||||
"SELECT id \
|
||||
.prepare(
|
||||
"SELECT id \
|
||||
FROM locations \
|
||||
WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND timestamp>? \
|
||||
AND independent=0 \
|
||||
ORDER BY timestamp;",
|
||||
)?;
|
||||
|
||||
let mut msgs = Vec::new();
|
||||
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
|
||||
if !stmt_locations
|
||||
.exists(paramsv![
|
||||
DC_CONTACT_ID_SELF,
|
||||
*locations_send_begin,
|
||||
*locations_last_sent,
|
||||
])
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
msgs.push((*chat_id, msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msgs)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|mut stmt_locations, _| {
|
||||
let msgs = rows
|
||||
.into_iter()
|
||||
.filter_map(|(chat_id, locations_send_begin, locations_last_sent)| {
|
||||
if !stmt_locations
|
||||
.exists(params![
|
||||
DC_CONTACT_ID_SELF,
|
||||
locations_send_begin,
|
||||
locations_last_sent,
|
||||
])
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
None
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
Some((chat_id, msg))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(msgs)
|
||||
},
|
||||
)
|
||||
.unwrap_or_default(); // TODO: Better error handling
|
||||
|
||||
for (chat_id, mut msg) in msgs.into_iter() {
|
||||
// TODO: better error handling
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
if continue_streaming {
|
||||
schedule_maybe_send_locations(context, true).await;
|
||||
schedule_MAYBE_SEND_LOCATIONS(context, true);
|
||||
}
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations_ended(
|
||||
context: &Context,
|
||||
job: &mut Job,
|
||||
) -> job::Status {
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
|
||||
// this function is called when location-streaming _might_ have ended for a chat.
|
||||
// the function checks, if location-streaming is really ended;
|
||||
// if so, a device-message is added if not yet done.
|
||||
|
||||
let chat_id = ChatId::new(job.foreign_id);
|
||||
|
||||
let (send_begin, send_until) = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
);
|
||||
let (send_begin, send_until) = job_try!(context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
params![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
));
|
||||
|
||||
if !(send_begin != 0 && time() <= send_until) {
|
||||
// still streaming -
|
||||
@@ -706,14 +660,12 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
// not streaming, device-message already sent
|
||||
job_try!(context.sql.execute(
|
||||
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
).await);
|
||||
params![chat_id],
|
||||
));
|
||||
|
||||
let stock_str = context
|
||||
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, stock_str).await;
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
|
||||
chat::add_info_msg(context, chat_id, stock_str);
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
job::Status::Finished(Ok(()))
|
||||
@@ -724,9 +676,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::dummy_context;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_kml_parse() {
|
||||
let context = dummy_context().await;
|
||||
#[test]
|
||||
fn test_kml_parse() {
|
||||
let context = dummy_context();
|
||||
|
||||
let xml =
|
||||
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
|
||||
|
||||
10
src/log.rs
10
src/log.rs
@@ -7,7 +7,9 @@ macro_rules! info {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
let thread = ::std::thread::current();
|
||||
let full = format!("{thid:?} {file}:{line}: {msg}",
|
||||
thid = thread.id(),
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
@@ -22,7 +24,9 @@ macro_rules! warn {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
let thread = ::std::thread::current();
|
||||
let full = format!("{thid:?} {file}:{line}: {msg}",
|
||||
thid = thread.id(),
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
@@ -44,6 +48,6 @@ macro_rules! error {
|
||||
#[macro_export]
|
||||
macro_rules! emit_event {
|
||||
($ctx:expr, $event:expr) => {
|
||||
$ctx.emit_event($event);
|
||||
$ctx.call_cb($event);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,69 +50,59 @@ impl LoginParam {
|
||||
}
|
||||
|
||||
/// Read the login parameters from the database.
|
||||
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
|
||||
pub fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
|
||||
let prefix = prefix.as_ref();
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
let addr = sql
|
||||
.get_raw_config(context, key)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_server = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
let mail_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mail_port = sql.get_raw_config_int(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_user = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_pw = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let imap_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_server = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
let send_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let send_port = sql.get_raw_config_int(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_user = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_pw = sql.get_raw_config(context, key).unwrap_or_default();
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
let server_flags = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let server_flags = sql.get_raw_config_int(context, key).unwrap_or_default();
|
||||
|
||||
LoginParam {
|
||||
addr,
|
||||
@@ -130,8 +120,12 @@ impl LoginParam {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_str(&self) -> &str {
|
||||
self.addr.as_str()
|
||||
}
|
||||
|
||||
/// Save this loginparam to the database.
|
||||
pub async fn save_to_database(
|
||||
pub fn save_to_database(
|
||||
&self,
|
||||
context: &Context,
|
||||
prefix: impl AsRef<str>,
|
||||
@@ -140,49 +134,40 @@ impl LoginParam {
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.addr)).await?;
|
||||
sql.set_raw_config(context, key, Some(&self.addr))?;
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_server))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.mail_server))?;
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.mail_port).await?;
|
||||
sql.set_raw_config_int(context, key, self.mail_port)?;
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_user))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.mail_user))?;
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_pw))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.mail_pw))?;
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)?;
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_server))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.send_server))?;
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.send_port).await?;
|
||||
sql.set_raw_config_int(context, key, self.send_port)?;
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_user))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.send_user))?;
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_pw))
|
||||
.await?;
|
||||
sql.set_raw_config(context, key, Some(&self.send_pw))?;
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)?;
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
sql.set_raw_config_int(context, key, self.server_flags)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, key, self.server_flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -273,15 +258,21 @@ fn get_readable_flags(flags: i32) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
|
||||
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
|
||||
let tls_builder = async_native_tls::TlsConnector::new();
|
||||
|
||||
if strict_tls {
|
||||
tls_builder
|
||||
} else {
|
||||
tls_builder
|
||||
match certificate_checks {
|
||||
CertificateChecks::Automatic => {
|
||||
// Same as AcceptInvalidCertificates for now.
|
||||
// TODO: use provider database when it becomes available
|
||||
tls_builder
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true)
|
||||
}
|
||||
CertificateChecks::Strict => tls_builder,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_certs(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
/// Lot objects are created
|
||||
@@ -16,7 +14,7 @@ pub struct Lot {
|
||||
pub(crate) timestamp: i64,
|
||||
pub(crate) state: LotState,
|
||||
pub(crate) id: u32,
|
||||
pub(crate) fingerprint: Option<Fingerprint>,
|
||||
pub(crate) fingerprint: Option<String>,
|
||||
pub(crate) invitenumber: Option<String>,
|
||||
pub(crate) auth: Option<String>,
|
||||
}
|
||||
@@ -42,11 +40,11 @@ impl Lot {
|
||||
}
|
||||
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
self.text1.as_deref()
|
||||
self.text1.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<&str> {
|
||||
self.text2.as_deref()
|
||||
self.text2.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn get_text1_meaning(&self) -> Meaning {
|
||||
|
||||
772
src/message.rs
772
src/message.rs
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,12 @@ use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::error::Error;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
// attachments of 25 mb brutto should work on the majority of providers
|
||||
@@ -50,7 +49,6 @@ pub struct MimeFactory<'a, 'b> {
|
||||
context: &'a Context,
|
||||
last_added_location_id: u32,
|
||||
attach_selfavatar: bool,
|
||||
upload_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
@@ -67,89 +65,89 @@ pub struct RenderedEmail {
|
||||
}
|
||||
|
||||
impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
pub async fn from_msg(
|
||||
pub fn from_msg(
|
||||
context: &'a Context,
|
||||
msg: &'b Message,
|
||||
attach_selfavatar: bool,
|
||||
) -> Result<MimeFactory<'a, 'b>, Error> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
let chat = Chat::load_from_db(context, msg.chat_id)?;
|
||||
|
||||
let from_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let from_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let from_displayname = context.get_config(Config::Displayname).unwrap_or_default();
|
||||
let mut recipients = Vec::with_capacity(5);
|
||||
let mut req_mdn = false;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
context.sql.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
paramsv![msg.chat_id],
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (authname, addr) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
params![msg.chat_id],
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (authname, addr) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
let command = msg.param.get_cmd();
|
||||
|
||||
/* for added members, the list is just fine */
|
||||
if command == SystemMessage::MemberRemovedFromGroup {
|
||||
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !email_to_remove.is_empty()
|
||||
&& !addr_cmp(email_to_remove, self_addr)
|
||||
&& !recipients_contain_addr(&recipients, &email_to_remove)
|
||||
{
|
||||
recipients.push(("".to_string(), email_to_remove.to_string()));
|
||||
}
|
||||
}
|
||||
if command != SystemMessage::AutocryptSetupMessage
|
||||
&& command != SystemMessage::SecurejoinMessage
|
||||
&& context.get_config_bool(Config::MdnsEnabled).await
|
||||
&& context.get_config_bool(Config::MdnsEnabled)
|
||||
{
|
||||
req_mdn = true;
|
||||
}
|
||||
}
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
paramsv![msg.id],
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
let (in_reply_to, references) = context.sql.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
params![msg.id],
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
|
||||
Ok((
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
let default_str = context
|
||||
.stock_str(StockMessage::StatusLine)
|
||||
.await
|
||||
.to_string();
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
selfstatus: context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.unwrap_or_else(|| default_str),
|
||||
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()),
|
||||
recipients,
|
||||
timestamp: msg.timestamp_sort,
|
||||
loaded: Loaded::Message { chat },
|
||||
@@ -160,47 +158,33 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
last_added_location_id: 0,
|
||||
attach_selfavatar,
|
||||
context,
|
||||
upload_url: None,
|
||||
};
|
||||
Ok(factory)
|
||||
}
|
||||
|
||||
pub async fn from_mdn(
|
||||
pub fn from_mdn(
|
||||
context: &'a Context,
|
||||
msg: &'b Message,
|
||||
additional_msg_ids: Vec<String>,
|
||||
) -> Result<MimeFactory<'a, 'b>, Error> {
|
||||
) -> Result<Self, Error> {
|
||||
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let contact = Contact::load_from_db(context, msg.from_id).await?;
|
||||
let from_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let from_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let default_str = context
|
||||
.stock_str(StockMessage::StatusLine)
|
||||
.await
|
||||
.to_string();
|
||||
let selfstatus = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.unwrap_or_else(|| default_str);
|
||||
let timestamp = dc_create_smeared_timestamp(context).await;
|
||||
let contact = Contact::load_from_db(context, msg.from_id)?;
|
||||
|
||||
let res = MimeFactory::<'a, 'b> {
|
||||
Ok(MimeFactory {
|
||||
context,
|
||||
from_addr,
|
||||
from_displayname,
|
||||
selfstatus,
|
||||
from_addr: context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default(),
|
||||
from_displayname: context.get_config(Config::Displayname).unwrap_or_default(),
|
||||
selfstatus: context
|
||||
.get_config(Config::Selfstatus)
|
||||
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()),
|
||||
recipients: vec![(
|
||||
contact.get_authname().to_string(),
|
||||
contact.get_addr().to_string(),
|
||||
)],
|
||||
timestamp,
|
||||
timestamp: dc_create_smeared_timestamp(context),
|
||||
loaded: Loaded::MDN { additional_msg_ids },
|
||||
msg,
|
||||
in_reply_to: String::default(),
|
||||
@@ -208,32 +192,26 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
req_mdn: false,
|
||||
last_added_location_id: 0,
|
||||
attach_selfavatar: false,
|
||||
upload_url: None,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
|
||||
fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
|
||||
let self_addr = self
|
||||
.context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for (_, addr) in self
|
||||
Ok(self
|
||||
.recipients
|
||||
.iter()
|
||||
.filter(|(_, addr)| addr != &self_addr)
|
||||
{
|
||||
res.push((
|
||||
Peerstate::from_addr(self.context, addr).await,
|
||||
addr.as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
.map(|(_, addr)| {
|
||||
(
|
||||
Peerstate::from_addr(self.context, &self.context.sql, addr),
|
||||
addr.as_str(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn is_e2ee_guaranteed(&self) -> bool {
|
||||
@@ -293,11 +271,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_do_gossip(&self) -> bool {
|
||||
fn should_do_gossip(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
// beside key- and member-changes, force re-gossip every 48 hours
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context).await;
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context);
|
||||
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
|
||||
return true;
|
||||
}
|
||||
@@ -338,13 +316,12 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn subject_str(&self) -> String {
|
||||
fn subject_str(&self) -> String {
|
||||
match self.loaded {
|
||||
Loaded::Message { ref chat } => {
|
||||
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
self.context
|
||||
.stock_str(StockMessage::AcSetupMsgSubject)
|
||||
.await
|
||||
.into_owned()
|
||||
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
@@ -354,54 +331,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
};
|
||||
format!("{}{}", re, chat.name)
|
||||
} else {
|
||||
match chat.param.get(Param::LastSubject) {
|
||||
Some(last_subject) => {
|
||||
let subject_start = if last_subject.starts_with("Chat:") {
|
||||
0
|
||||
} else {
|
||||
// "Antw:" is the longest abbreviation in
|
||||
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
|
||||
// so look at the first _5_ characters:
|
||||
match last_subject.chars().take(5).position(|c| c == ':') {
|
||||
Some(prefix_end) => prefix_end + 1,
|
||||
None => 0,
|
||||
}
|
||||
};
|
||||
format!(
|
||||
"Re: {}",
|
||||
last_subject
|
||||
.chars()
|
||||
.skip(subject_start)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
None => {
|
||||
let self_name = match self.context.get_config(Config::Displayname).await
|
||||
{
|
||||
Some(name) => name,
|
||||
None => self
|
||||
.context
|
||||
.get_config(Config::Addr)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
self.context
|
||||
.stock_string_repl_str(
|
||||
StockMessage::SubjectForNewContact,
|
||||
self_name,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
let raw = message::get_summarytext_by_raw(
|
||||
self.msg.viewtype,
|
||||
self.msg.text.as_ref(),
|
||||
&self.msg.param,
|
||||
32,
|
||||
self.context,
|
||||
);
|
||||
let raw_subject = raw.lines().next().unwrap_or_default();
|
||||
format!("Chat: {}", raw_subject)
|
||||
}
|
||||
}
|
||||
Loaded::MDN { .. } => self
|
||||
.context
|
||||
.stock_str(StockMessage::ReadRcpt)
|
||||
.await
|
||||
.into_owned(),
|
||||
Loaded::MDN { .. } => self.context.stock_str(StockMessage::ReadRcpt).into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,11 +353,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_upload_url(&mut self, upload_url: String) {
|
||||
self.upload_url = Some(upload_url)
|
||||
}
|
||||
|
||||
pub async fn render(mut self) -> Result<RenderedEmail, Error> {
|
||||
pub fn render(mut self) -> Result<RenderedEmail, Error> {
|
||||
// Headers that are encrypted
|
||||
// - Chat-*, except Chat-Version
|
||||
// - Secure-Join*
|
||||
@@ -431,7 +368,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
let mut to = Vec::new();
|
||||
let mut to = Vec::with_capacity(self.recipients.len());
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
@@ -443,12 +380,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
|
||||
unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||
|
||||
if !self.references.is_empty() {
|
||||
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
|
||||
}
|
||||
@@ -501,18 +432,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let min_verified = self.min_verified();
|
||||
let grpimage = self.grpimage();
|
||||
let force_plaintext = self.should_force_plaintext();
|
||||
let subject_str = self.subject_str().await;
|
||||
let subject_str = self.subject_str();
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(self.context).await?;
|
||||
let mut encrypt_helper = EncryptHelper::new(self.context)?;
|
||||
|
||||
let subject = encode_words(&subject_str);
|
||||
|
||||
let mut message = match self.loaded {
|
||||
Loaded::Message { .. } => {
|
||||
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)
|
||||
.await?
|
||||
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)?
|
||||
}
|
||||
Loaded::MDN { .. } => self.render_mdn().await?,
|
||||
Loaded::MDN { .. } => self.render_mdn()?,
|
||||
};
|
||||
|
||||
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
|
||||
@@ -523,7 +453,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
protected_headers.push(Header::new("Subject".into(), subject));
|
||||
|
||||
let peerstates = self.peerstates_for_recipients().await?;
|
||||
let peerstates = self.peerstates_for_recipients()?;
|
||||
let should_encrypt =
|
||||
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_encrypted = should_encrypt && force_plaintext == 0;
|
||||
@@ -551,7 +481,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
// Add gossip headers in chats with multiple recipients
|
||||
if peerstates.len() > 1 && self.should_do_gossip().await {
|
||||
if peerstates.len() > 1 && self.should_do_gossip() {
|
||||
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
||||
if peerstate.peek_key(min_verified).is_some() {
|
||||
if let Some(header) = peerstate.render_gossip_header(min_verified) {
|
||||
@@ -599,9 +529,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
println!("{}", raw_message);
|
||||
}
|
||||
|
||||
let encrypted = encrypt_helper
|
||||
.encrypt(self.context, min_verified, message, peerstates)
|
||||
.await?;
|
||||
let encrypted =
|
||||
encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?;
|
||||
|
||||
outer_message = outer_message
|
||||
.child(
|
||||
@@ -673,9 +602,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Some(part)
|
||||
}
|
||||
|
||||
async fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
|
||||
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
|
||||
let (kml_content, last_added_location_id) =
|
||||
location::get_kml(self.context, self.msg.chat_id).await?;
|
||||
location::get_kml(self.context, self.msg.chat_id)?;
|
||||
let part = PartBuilder::new()
|
||||
.content_type(
|
||||
&"application/vnd.google-earth.kml+xml"
|
||||
@@ -695,7 +624,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn render_message(
|
||||
fn render_message(
|
||||
&mut self,
|
||||
protected_headers: &mut Vec<Header>,
|
||||
unprotected_headers: &mut Vec<Header>,
|
||||
@@ -790,7 +719,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
placeholdertext = Some(
|
||||
self.context
|
||||
.stock_str(StockMessage::AcSetupMsgBody)
|
||||
.await
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -837,7 +765,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
meta.viewtype = Viewtype::Image;
|
||||
meta.param.set(Param::File, grpimage);
|
||||
|
||||
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?;
|
||||
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
|
||||
meta_part = Some(mail);
|
||||
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
|
||||
}
|
||||
@@ -886,21 +814,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
};
|
||||
|
||||
// if upload url is present: add as header and to message text
|
||||
// TODO: make text part translatable (or remove)
|
||||
let upload_url_text = if let Some(ref upload_url) = self.upload_url {
|
||||
protected_headers.push(Header::new("Chat-Upload-Url".into(), upload_url.clone()));
|
||||
Some(format!("\n\nFile attachement: {}", upload_url.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let footer = &self.selfstatus;
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
"{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
escape_message_footer_marks(final_text),
|
||||
upload_url_text.unwrap_or_default(),
|
||||
&final_text,
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
@@ -916,15 +834,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.body(message_text);
|
||||
let mut parts = Vec::new();
|
||||
|
||||
// add attachment part, skip if upload url was provided
|
||||
if chat::msgtype_has_file(self.msg.viewtype) && self.upload_url.is_none() {
|
||||
if !is_file_size_okay(context, &self.msg).await {
|
||||
// add attachment part
|
||||
if chat::msgtype_has_file(self.msg.viewtype) {
|
||||
if !is_file_size_okay(context, &self.msg) {
|
||||
bail!(
|
||||
"Message exceeds the recommended {} MB.",
|
||||
RECOMMENDED_FILE_SIZE / 1_000_000,
|
||||
);
|
||||
} else {
|
||||
let (file_part, _) = build_body_file(context, &self.msg, "").await?;
|
||||
let (file_part, _) = build_body_file(context, &self.msg, "")?;
|
||||
parts.push(file_part);
|
||||
}
|
||||
}
|
||||
@@ -937,8 +855,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, self.msg.chat_id).await {
|
||||
match self.get_location_kml_part().await {
|
||||
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
|
||||
match self.get_location_kml_part() {
|
||||
Ok(part) => parts.push(part),
|
||||
Err(err) => {
|
||||
warn!(context, "mimefactory: could not send location: {}", err);
|
||||
@@ -947,7 +865,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
|
||||
if self.attach_selfavatar {
|
||||
match context.get_config(Config::Selfavatar).await {
|
||||
match context.get_config(Config::Selfavatar) {
|
||||
Some(path) => match build_selfavatar_file(context, &path) {
|
||||
Ok((part, filename)) => {
|
||||
parts.push(part);
|
||||
@@ -974,7 +892,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
|
||||
/// Render an MDN
|
||||
async fn render_mdn(&mut self) -> Result<PartBuilder, Error> {
|
||||
fn render_mdn(&mut self) -> Result<PartBuilder, Error> {
|
||||
// RFC 6522, this also requires the `report-type` parameter which is equal
|
||||
// to the MIME subtype of the second body part of the multipart/report
|
||||
//
|
||||
@@ -1009,15 +927,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
{
|
||||
self.context
|
||||
.stock_str(StockMessage::EncryptedMsg)
|
||||
.await
|
||||
.into_owned()
|
||||
} else {
|
||||
self.msg.get_summarytext(self.context, 32).await
|
||||
self.msg.get_summarytext(self.context, 32)
|
||||
};
|
||||
let p2 = self
|
||||
.context
|
||||
.stock_string_repl_str(StockMessage::ReadRcptMailBody, p1)
|
||||
.await;
|
||||
.stock_string_repl_str(StockMessage::ReadRcptMailBody, p1);
|
||||
let message_text = format!("{}\r\n", p2);
|
||||
message = message.child(
|
||||
PartBuilder::new()
|
||||
@@ -1074,15 +990,14 @@ fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
.join("\r\n")
|
||||
}
|
||||
|
||||
async fn build_body_file(
|
||||
fn build_body_file(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
base_name: &str,
|
||||
) -> Result<(PartBuilder, String), Error> {
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context, true)
|
||||
.await?
|
||||
.get_blob(Param::File, context, true)?
|
||||
.ok_or_else(|| format_err!("msg has no filename"))?;
|
||||
let suffix = blob.suffix().unwrap_or("dat");
|
||||
|
||||
@@ -1178,10 +1093,10 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool
|
||||
.any(|(_, cur)| cur.to_lowercase() == addr_lc)
|
||||
}
|
||||
|
||||
async fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
match msg.param.get_path(Param::File, context).unwrap_or(None) {
|
||||
Some(path) => {
|
||||
let bytes = dc_get_filebytes(context, &path).await;
|
||||
let bytes = dc_get_filebytes(context, &path);
|
||||
bytes <= UPPER_LIMIT_FILE_SIZE
|
||||
}
|
||||
None => false,
|
||||
@@ -1191,7 +1106,7 @@ async fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
fn render_rfc724_mid(rfc724_mid: &str) -> String {
|
||||
let rfc724_mid = rfc724_mid.trim().to_string();
|
||||
|
||||
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
|
||||
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
|
||||
rfc724_mid
|
||||
} else {
|
||||
format!("<{}>", rfc724_mid)
|
||||
@@ -1224,11 +1139,6 @@ pub fn needs_encoding(to_check: impl AsRef<str>) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::mimeparser::*;
|
||||
use crate::test_utils::configured_offline_context;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
@@ -1236,9 +1146,6 @@ mod tests {
|
||||
let addr = "x@y.org";
|
||||
|
||||
assert!(!display_name.is_ascii());
|
||||
assert!(!display_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
|
||||
|
||||
let s = format!(
|
||||
"{}",
|
||||
@@ -1250,25 +1157,6 @@ mod tests {
|
||||
assert_eq!(s, "=?utf-8?q?=C3=A4_space?= <x@y.org>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address_noescape() {
|
||||
let display_name = "a space";
|
||||
let addr = "x@y.org";
|
||||
|
||||
assert!(display_name.is_ascii());
|
||||
assert!(display_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
|
||||
|
||||
let s = format!(
|
||||
"{}",
|
||||
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
|
||||
);
|
||||
|
||||
// Addresses should not be unnecessarily be encoded, see https://github.com/deltachat/deltachat-core-rust/issues/1575:
|
||||
assert_eq!(s, "a space <x@y.org>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_rfc724_mid() {
|
||||
assert_eq!(
|
||||
@@ -1311,192 +1199,4 @@ mod tests {
|
||||
assert!(needs_encoding(" "));
|
||||
assert!(needs_encoding("foo bar"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_subject() {
|
||||
// 1.: Receive a mail from an MUA or Delta Chat
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Antw: Chat: hello\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
)
|
||||
.await,
|
||||
"Re: Chat: hello"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Infos: 42\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
)
|
||||
.await,
|
||||
"Re: Infos: 42"
|
||||
);
|
||||
|
||||
// 2. Receive a message from Delta Chat when we did not send any messages before
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
)
|
||||
.await,
|
||||
"Re: Chat: hello"
|
||||
);
|
||||
|
||||
// 3. Send the first message to a new contact
|
||||
let t = configured_offline_context().await;
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
|
||||
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(first_subject_str(t).await, "Message from Alice");
|
||||
|
||||
// 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result)
|
||||
msg_to_subject_str(
|
||||
"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: äääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
msg_to_subject_str(
|
||||
"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: aäääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn first_subject_str(t: TestContext) -> String {
|
||||
let contact_id =
|
||||
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.org", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let chat_id = chat::create_by_contact_id(&t.ctx, contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text(Some("Hi".to_string()));
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::prepare_msg(&t.ctx, chat_id, &mut new_msg)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
mf.subject_str().await
|
||||
}
|
||||
|
||||
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
|
||||
let t = configured_offline_context().await;
|
||||
let new_msg = incoming_msg_to_reply_msg(imf_raw, &t.ctx).await;
|
||||
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
|
||||
.await
|
||||
.unwrap();
|
||||
mf.subject_str().await
|
||||
}
|
||||
|
||||
// Creates a mimefactory for a message that replies "Hi" to the incoming message in `imf_raw`.
|
||||
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dc_receive_imf(context, imf_raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
|
||||
let chat_id = chat::create_by_msg_id(context, chats.get_msg_id(0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text(Some("Hi".to_string()));
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::prepare_msg(context, chat_id, &mut new_msg)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
new_msg
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
// This test could still be extended
|
||||
async fn test_render_reply() {
|
||||
let t = configured_offline_context().await;
|
||||
let context = &t.ctx;
|
||||
|
||||
let msg = incoming_msg_to_reply_msg(
|
||||
b"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(&t.ctx, &msg, false).await.unwrap();
|
||||
|
||||
let recipients = mimefactory.recipients();
|
||||
assert_eq!(recipients, vec!["charlie@example.org"]);
|
||||
|
||||
let rendered_msg = mimefactory.render().await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&rendered_msg.message).unwrap();
|
||||
assert_eq!(
|
||||
mail.headers
|
||||
.iter()
|
||||
.find(|h| h.get_key() == "MIME-Version")
|
||||
.unwrap()
|
||||
.get_value(),
|
||||
"1.0"
|
||||
);
|
||||
|
||||
let _mime_msg = MimeMessage::from_bytes(context, &rendered_msg.message)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
127
src/oauth2.rs
127
src/oauth2.rs
@@ -48,7 +48,7 @@ struct Response {
|
||||
scope: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn dc_get_oauth2_url(
|
||||
pub fn dc_get_oauth2_url(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
redirect_uri: impl AsRef<str>,
|
||||
@@ -61,7 +61,6 @@ pub async fn dc_get_oauth2_url(
|
||||
"oauth2_pending_redirect_uri",
|
||||
Some(redirect_uri.as_ref()),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
@@ -75,21 +74,21 @@ pub async fn dc_get_oauth2_url(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_get_oauth2_access_token(
|
||||
// The following function may block due http-requests;
|
||||
// must not be called from the main thread or by the ui!
|
||||
pub fn dc_get_oauth2_access_token(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
code: impl AsRef<str>,
|
||||
regenerate: bool,
|
||||
) -> Option<String> {
|
||||
if let Some(oauth2) = Oauth2::from_address(addr) {
|
||||
let lock = context.oauth2_mutex.lock().await;
|
||||
let lock = context.oauth2_critical.clone();
|
||||
let _l = lock.lock().unwrap();
|
||||
|
||||
// read generated token
|
||||
if !regenerate && !is_expired(context).await {
|
||||
let access_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_access_token")
|
||||
.await;
|
||||
if !regenerate && !is_expired(context) {
|
||||
let access_token = context.sql.get_raw_config(context, "oauth2_access_token");
|
||||
if access_token.is_some() {
|
||||
// success
|
||||
return access_token;
|
||||
@@ -97,14 +96,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
}
|
||||
|
||||
// generate new token: build & call auth url
|
||||
let refresh_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token")
|
||||
.await;
|
||||
let refresh_token = context.sql.get_raw_config(context, "oauth2_refresh_token");
|
||||
let refresh_token_for = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token_for")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into());
|
||||
|
||||
let (redirect_uri, token_url, update_redirect_uri_on_success) =
|
||||
@@ -114,7 +109,6 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_pending_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.init_token,
|
||||
true,
|
||||
@@ -128,7 +122,6 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.refresh_token,
|
||||
false,
|
||||
@@ -161,7 +154,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
}
|
||||
|
||||
// ... and POST
|
||||
let response = surf::post(post_url).body_form(&post_param);
|
||||
let response = reqwest::blocking::Client::new()
|
||||
.post(post_url)
|
||||
.form(&post_param)
|
||||
.send();
|
||||
if response.is_err() {
|
||||
warn!(
|
||||
context,
|
||||
@@ -169,8 +165,19 @@ pub async fn dc_get_oauth2_access_token(
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let response = response.unwrap();
|
||||
if !response.status().is_success() {
|
||||
warn!(
|
||||
context,
|
||||
"Unsuccessful response when calling OAuth2 at {}: {:?}",
|
||||
token_url,
|
||||
response.status()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed: Result<Response, _> = response.unwrap().recv_json().await;
|
||||
// generate new token: parse returned json
|
||||
let parsed: reqwest::Result<Response> = response.json();
|
||||
if parsed.is_err() {
|
||||
warn!(
|
||||
context,
|
||||
@@ -178,6 +185,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
);
|
||||
return None;
|
||||
}
|
||||
println!("response: {:?}", &parsed);
|
||||
|
||||
// update refresh_token if given, typically on the first round, but we update it later as well.
|
||||
let response = parsed.unwrap();
|
||||
@@ -185,12 +193,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_refresh_token", Some(token))
|
||||
.await
|
||||
.ok();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -200,7 +206,6 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_access_token", Some(token))
|
||||
.await
|
||||
.ok();
|
||||
let expires_in = response
|
||||
.expires_in
|
||||
@@ -210,22 +215,18 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if update_redirect_uri_on_success {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Failed to find OAuth2 access token");
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
|
||||
response.access_token
|
||||
} else {
|
||||
warn!(context, "Internal OAuth2 error: 2");
|
||||
@@ -234,7 +235,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_get_oauth2_addr(
|
||||
pub fn dc_get_oauth2_addr(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
code: impl AsRef<str>,
|
||||
@@ -243,14 +244,13 @@ pub async fn dc_get_oauth2_addr(
|
||||
oauth2.get_userinfo?;
|
||||
|
||||
if let Some(access_token) =
|
||||
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
|
||||
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false)
|
||||
{
|
||||
let addr_out = oauth2.get_addr(context, access_token).await;
|
||||
let addr_out = oauth2.get_addr(context, access_token);
|
||||
if addr_out.is_none() {
|
||||
// regenerate
|
||||
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
|
||||
{
|
||||
oauth2.get_addr(context, access_token).await
|
||||
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true) {
|
||||
oauth2.get_addr(context, access_token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -280,7 +280,7 @@ impl Oauth2 {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
|
||||
fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
|
||||
let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
|
||||
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
|
||||
|
||||
@@ -291,35 +291,50 @@ impl Oauth2 {
|
||||
// "verified_email": true,
|
||||
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
|
||||
// }
|
||||
let response: Result<HashMap<String, serde_json::Value>, surf::Error> =
|
||||
surf::get(userinfo_url).recv_json().await;
|
||||
let response = reqwest::blocking::Client::new().get(&userinfo_url).send();
|
||||
if response.is_err() {
|
||||
warn!(context, "Error getting userinfo: {:?}", response);
|
||||
return None;
|
||||
}
|
||||
let response = response.unwrap();
|
||||
if !response.status().is_success() {
|
||||
warn!(context, "Error getting userinfo: {:?}", response.status());
|
||||
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") {
|
||||
if let Some(s) = addr.as_str() {
|
||||
Some(s.to_string())
|
||||
let parsed: reqwest::Result<HashMap<String, serde_json::Value>> = response.json();
|
||||
if parsed.is_err() {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse userinfo JSON response: {:?}", parsed
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if let Ok(response) = parsed {
|
||||
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// but serde_json::Value.to_string() does not!
|
||||
if let Some(addr) = response.get("email") {
|
||||
if let Some(s) = addr.as_str() {
|
||||
Some(s.to_string())
|
||||
} else {
|
||||
warn!(context, "E-mail in userinfo is not a string: {}", addr);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
warn!(context, "E-mail in userinfo is not a string: {}", addr);
|
||||
warn!(context, "E-mail missing in userinfo.");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
warn!(context, "E-mail missing in userinfo.");
|
||||
warn!(context, "Failed to parse userinfo.");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_expired(context: &Context) -> bool {
|
||||
fn is_expired(context: &Context) -> bool {
|
||||
let expire_timestamp = context
|
||||
.sql
|
||||
.get_raw_config_int64(context, "oauth2_timestamp_expires")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if expire_timestamp <= 0 {
|
||||
@@ -378,32 +393,32 @@ mod tests {
|
||||
assert_eq!(Oauth2::from_address("hello@web.de"), None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_addr() {
|
||||
let ctx = dummy_context().await;
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_addr() {
|
||||
let ctx = dummy_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_url() {
|
||||
let ctx = dummy_context().await;
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_url() {
|
||||
let ctx = dummy_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let redirect_uri = "chat.delta:/com.b44t.messenger";
|
||||
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
|
||||
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri);
|
||||
|
||||
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_token() {
|
||||
let ctx = dummy_context().await;
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_token() {
|
||||
let ctx = dummy_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user