mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
98 Commits
testing-on
...
provider_t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7835ae2d0 | ||
|
|
9f988f68a1 | ||
|
|
e387b4f4dd | ||
|
|
88a10eaf2c | ||
|
|
4aae12ead7 | ||
|
|
89c985120b | ||
|
|
40bb0616da | ||
|
|
2b81c274f1 | ||
|
|
0266b70b23 | ||
|
|
9c0d84090e | ||
|
|
baf7e98c1e | ||
|
|
2d20a81f22 | ||
|
|
4be4472dfb | ||
|
|
98c1158cde | ||
|
|
3769ad32bd | ||
|
|
1c436777e0 | ||
|
|
adac903818 | ||
|
|
03f0659454 | ||
|
|
296c230bc9 | ||
|
|
ffd00978e9 | ||
|
|
a8f58ec2cf | ||
|
|
d8a2c05c71 | ||
|
|
7e4386c197 | ||
|
|
f595264418 | ||
|
|
f3e8f5babc | ||
|
|
d7b4a5fc9e | ||
|
|
be413b20f1 | ||
|
|
99d9773b75 | ||
|
|
633929b84c | ||
|
|
f42da17a78 | ||
|
|
d421670477 | ||
|
|
c4f36836d4 | ||
|
|
553f4c4b88 | ||
|
|
30c463e0ba | ||
|
|
d5c1e26354 | ||
|
|
b7864f232b | ||
|
|
8e9d8ae1ec | ||
|
|
f52c23d1c7 | ||
|
|
957f942872 | ||
|
|
6971bfc3d4 | ||
|
|
16dcd712f0 | ||
|
|
9f337e8be5 | ||
|
|
c4217ea929 | ||
|
|
3a742f1d09 | ||
|
|
ae0dbf024d | ||
|
|
01d3611f3b | ||
|
|
f1608b503f | ||
|
|
98beb7f40c | ||
|
|
574bb8fd7f | ||
|
|
f5de2e7684 | ||
|
|
42086ceec5 | ||
|
|
cfb22c23df | ||
|
|
d49de4b3e4 | ||
|
|
540ad71473 | ||
|
|
b27ad955f8 | ||
|
|
5546ed772e | ||
|
|
23e891f051 | ||
|
|
7dd5b05a00 | ||
|
|
b7d274e0f9 | ||
|
|
437b7ef1f1 | ||
|
|
6934947d0d | ||
|
|
d920ec96fa | ||
|
|
ebfeec8907 | ||
|
|
6d064dca84 | ||
|
|
2c2fad6f28 | ||
|
|
60b4f3f21a | ||
|
|
c128e54896 | ||
|
|
0ea6f72624 | ||
|
|
855b6b18fd | ||
|
|
abac35c872 | ||
|
|
17ad4e99ee | ||
|
|
c5aef03008 | ||
|
|
c7f2a43654 | ||
|
|
19176d9d47 | ||
|
|
db1a7023eb | ||
|
|
ae31b5895b | ||
|
|
35b6dd797d | ||
|
|
1d708de82f | ||
|
|
f7139331e7 | ||
|
|
131651cc02 | ||
|
|
bba437523a | ||
|
|
f76bc44cdc | ||
|
|
f6eb169c60 | ||
|
|
e15ec2eb7a | ||
|
|
b3b46688fc | ||
|
|
9faf4a5fa7 | ||
|
|
628c30f130 | ||
|
|
f40b557454 | ||
|
|
e1b9e8f2c9 | ||
|
|
65c17cfea2 | ||
|
|
39d3a594af | ||
|
|
949e671d9c | ||
|
|
eef51f064a | ||
|
|
143c5e6249 | ||
|
|
8610b0c945 | ||
|
|
d179dced4e | ||
|
|
1dc055fb66 | ||
|
|
819775ac39 |
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: "cargo"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -114,8 +114,10 @@ jobs:
|
|||||||
|
|
||||||
- name: check
|
- name: check
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --all --bins --examples --tests --features repl
|
args: --all --bins --examples --tests --features repl
|
||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
|
|||||||
5
.github/workflows/remote_tests.yml
vendored
5
.github/workflows/remote_tests.yml
vendored
@@ -5,9 +5,6 @@ jobs:
|
|||||||
name: Remote Python tests
|
name: Remote Python tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CIRCLE_BRANCH: ${{ github.ref }}
|
|
||||||
CIRCLE_JOB: remote_tests_python
|
|
||||||
CIRCLE_BUILD_NUM: ${{ github.run_number }}
|
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -18,4 +15,4 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||||
- run: scripts/remote_tests_python.sh
|
- run: scripts/remote_tests_python.sh "deltachat-core/python/${{ github.ref }}/${{ github.run_number }}"
|
||||||
|
|||||||
32
.github/workflows/repl.yml
vendored
Normal file
32
.github/workflows/repl.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Manually triggered action to build a Windows repl.exe which users can
|
||||||
|
# download to debug complex bugs.
|
||||||
|
|
||||||
|
name: Build Windows REPL .exe
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_repl:
|
||||||
|
name: Build REPL example
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.50.0
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --example repl --features repl,vendored
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: repl.exe
|
||||||
|
path: 'target/debug/examples/repl.exe'
|
||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.55.0
|
||||||
|
|
||||||
|
- fix panic when receiving some HTML messages #2434
|
||||||
|
|
||||||
|
- fix downloading some messages multiple times #2430
|
||||||
|
|
||||||
|
- fix formatting of read receipt texts #2431
|
||||||
|
|
||||||
|
- simplify SQL error handling #2415
|
||||||
|
|
||||||
|
- explicit rust API for creating chats with blocked status #2282
|
||||||
|
|
||||||
|
- debloat the binary by using less AsRef arguments #2425
|
||||||
|
|
||||||
|
|
||||||
|
## 1.54.0
|
||||||
|
|
||||||
|
- switch back from `sqlx` to `rusqlite` due to performance regressions #2380 #2381 #2385 #2387
|
||||||
|
|
||||||
|
- global search performance improvement #2364 #2365 #2366
|
||||||
|
|
||||||
|
- improve SQLite performance with `PRAGMA synchronous=normal` #2382
|
||||||
|
|
||||||
|
- python: fix building of bindings against system-wide install of `libdeltachat` #2383 #2385
|
||||||
|
|
||||||
|
- python: list `requests` as a requirement #2390
|
||||||
|
|
||||||
|
- fix creation of many delete jobs when being offline #2372
|
||||||
|
|
||||||
|
- synchronize status between devices #2386
|
||||||
|
|
||||||
|
- deaddrop (contact requests) chat improvements #2373
|
||||||
|
|
||||||
|
- add "Forwarded:" to notification and chatlist summaries #2310
|
||||||
|
|
||||||
|
- place user avatar directly into `Chat-User-Avatar` header #2232 #2384
|
||||||
|
|
||||||
|
- improve tests #2360 #2362 #2370 #2377 #2387
|
||||||
|
|
||||||
|
- cleanup #2359 #2361 #2374 #2376 #2379 #2388
|
||||||
|
|
||||||
|
|
||||||
## 1.53.0
|
## 1.53.0
|
||||||
|
|
||||||
- fix sqlx performance regression #2355 2356
|
- fix sqlx performance regression #2355 2356
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ add_custom_command(
|
|||||||
"target/release/libdeltachat.a"
|
"target/release/libdeltachat.a"
|
||||||
"target/release/libdeltachat.so"
|
"target/release/libdeltachat.so"
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
"target/release/pkgconfig/deltachat.pc"
|
||||||
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --package deltachat_ffi --release
|
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --release --no-default-features
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
||||||
|
# Build in `deltachat-ffi` directory instead of using
|
||||||
|
# `--package deltachat_ffi` to avoid feature resolver version
|
||||||
|
# "1" bug which makes `--no-default-features` affect only
|
||||||
|
# `deltachat`, but not `deltachat-ffi` package.
|
||||||
|
#
|
||||||
|
# We can't enable version "2" resolver [1] because it is not
|
||||||
|
# stable yet on rust 1.50.0.
|
||||||
|
#
|
||||||
|
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(
|
add_custom_target(
|
||||||
|
|||||||
696
Cargo.lock
generated
696
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.53.0"
|
version = "1.55.0"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -12,26 +12,28 @@ debug = 0
|
|||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
|
||||||
ansi_term = { version = "0.12.1", optional = true }
|
ansi_term = { version = "0.12.1", optional = true }
|
||||||
anyhow = "1.0.28"
|
anyhow = "1.0.40"
|
||||||
async-imap = "0.4.0"
|
async-imap = "0.5.0"
|
||||||
async-native-tls = { version = "0.3.3" }
|
async-native-tls = { version = "0.3.3" }
|
||||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||||
async-std-resolver = "0.19.5"
|
async-std-resolver = "0.20.2"
|
||||||
async-std = { version = "~1.8.0", features = ["unstable"] }
|
async-std = { version = "~1.9.0", features = ["unstable"] }
|
||||||
async-tar = "0.3.0"
|
async-tar = "0.3.0"
|
||||||
async-trait = "0.1.31"
|
async-trait = "0.1.50"
|
||||||
backtrace = "0.3.33"
|
backtrace = "0.3.59"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bitflags = "1.1.0"
|
bitflags = "1.1.0"
|
||||||
byteorder = "1.3.1"
|
byteorder = "1.3.1"
|
||||||
charset = "0.1"
|
charset = "0.1"
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
dirs = { version = "3.0.1", optional=true }
|
dirs = { version = "3.0.2", optional=true }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||||
escaper = "0.1.0"
|
escaper = "0.1.1"
|
||||||
futures = "0.3.4"
|
futures = "0.3.14"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
indexmap = "1.3.0"
|
indexmap = "1.3.0"
|
||||||
@@ -40,7 +42,7 @@ kamadak-exif = "0.5"
|
|||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
libc = "0.2.51"
|
libc = "0.2.51"
|
||||||
log = {version = "0.4.8", optional = true }
|
log = {version = "0.4.8", optional = true }
|
||||||
mailparse = "0.13.0"
|
mailparse = "0.13.4"
|
||||||
native-tls = "0.2.3"
|
native-tls = "0.2.3"
|
||||||
num_cpus = "1.13.0"
|
num_cpus = "1.13.0"
|
||||||
num-derive = "0.3.0"
|
num-derive = "0.3.0"
|
||||||
@@ -49,43 +51,44 @@ once_cell = "1.4.1"
|
|||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
pgp = { version = "0.7.0", default-features = false }
|
pgp = { version = "0.7.0", default-features = false }
|
||||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||||
quick-xml = "0.18.1"
|
quick-xml = "0.22.0"
|
||||||
|
r2d2 = "0.8.9"
|
||||||
|
r2d2_sqlite = "0.18.0"
|
||||||
rand = "0.7.0"
|
rand = "0.7.0"
|
||||||
regex = "1.1.6"
|
regex = "1.4.6"
|
||||||
|
rusqlite = "0.25"
|
||||||
rust-hsluv = "0.1.4"
|
rust-hsluv = "0.1.4"
|
||||||
rustyline = { version = "4.1.0", optional = true }
|
rustyline = { version = "8.0.0", optional = true }
|
||||||
sanitize-filename = "0.3.0"
|
sanitize-filename = "0.3.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha-1 = "0.9.3"
|
sha-1 = "0.9.5"
|
||||||
sha2 = "0.9.0"
|
sha2 = "0.9.4"
|
||||||
smallvec = "1.0.0"
|
smallvec = "1.0.0"
|
||||||
sqlx = { git = "https://github.com/deltachat/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] }
|
stop-token = "0.2.0"
|
||||||
# keep in sync with sqlx
|
|
||||||
libsqlite3-sys = { version = "0.22.0", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] }
|
|
||||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
|
||||||
strum = "0.20.0"
|
strum = "0.20.0"
|
||||||
strum_macros = "0.20.1"
|
strum_macros = "0.20.1"
|
||||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||||
thiserror = "1.0.14"
|
thiserror = "1.0.14"
|
||||||
toml = "0.5.6"
|
toml = "0.5.6"
|
||||||
url = "2.1.1"
|
url = "2.2.2"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
|
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
futures-lite = "1.7.0"
|
futures-lite = "1.7.0"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.7.2"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
proptest = "0.10"
|
proptest = "1.0"
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"deltachat-ffi",
|
"deltachat-ffi",
|
||||||
|
"deltachat_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
@@ -115,5 +118,5 @@ harness = false
|
|||||||
default = []
|
default = []
|
||||||
internals = []
|
internals = []
|
||||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]
|
||||||
nightly = ["pgp/nightly"]
|
nightly = ["pgp/nightly"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.53.0"
|
version = "1.55.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -20,8 +20,8 @@ libc = "0.2"
|
|||||||
human-panic = "1.0.1"
|
human-panic = "1.0.1"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
async-std = "1.6.0"
|
async-std = "1.9.0"
|
||||||
anyhow = "1.0.28"
|
anyhow = "1.0.40"
|
||||||
thiserror = "1.0.14"
|
thiserror = "1.0.14"
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
|
|
||||||
|
|||||||
@@ -1078,9 +1078,9 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
|
|||||||
* or badge counters eg. on the app-icon.
|
* or badge counters eg. on the app-icon.
|
||||||
* The list is already sorted and starts with the most recent fresh message.
|
* The list is already sorted and starts with the most recent fresh message.
|
||||||
*
|
*
|
||||||
* Messages belonging to muted chats are not returned,
|
* Messages belonging to muted chats or to the deaddrop are not returned;
|
||||||
* as they should not be notified
|
* these messages should not be notified
|
||||||
* and also a badge counters should not include messages of muted chats.
|
* and also badge counters should not include these messages.
|
||||||
*
|
*
|
||||||
* To get the number of fresh messages for a single chat, muted or not,
|
* To get the number of fresh messages for a single chat, muted or not,
|
||||||
* use dc_get_fresh_msg_cnt().
|
* use dc_get_fresh_msg_cnt().
|
||||||
@@ -1104,7 +1104,8 @@ dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
|
|||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object as returned from dc_context_new().
|
* @param context The context object as returned from dc_context_new().
|
||||||
* @param chat_id The chat ID of which all messages should be marked as being noticed.
|
* @param chat_id The chat ID of which all messages should be marked as being noticed
|
||||||
|
* (this also works for the virtual chat ID DC_CHAT_ID_DEADDROP).
|
||||||
*/
|
*/
|
||||||
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
|
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
@@ -1593,13 +1594,22 @@ void dc_marknoticed_contact (dc_context_t* context, uint32_t co
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a message as _seen_, updates the IMAP state and
|
* Mark messages as presented to the user.
|
||||||
* sends MDNs. If the message is not in a real chat (e.g. a contact request), the
|
* Typically, UIs call this function on scrolling through the chatlist,
|
||||||
* message is only marked as NOTICED and no IMAP/MDNs is done. See also
|
* when the messages are presented at least for a little moment.
|
||||||
* dc_marknoticed_chat().
|
* The concrete action depends on the type of the chat and on the users settings
|
||||||
|
* (dc_msgs_presented() may be a better name therefore, but well :)
|
||||||
*
|
*
|
||||||
* Moreover, if messages belong to a chat with ephemeral messages enabled,
|
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||||
* the ephemeral timer is started for these messages.
|
* (if dc_set_config()-options `mdns_enabled` is set)
|
||||||
|
* and the internal state is changed to DC_STATE_IN_SEEN to reflect these actions.
|
||||||
|
*
|
||||||
|
* - For the deaddrop, no IMAP or MNDs is done
|
||||||
|
* and the internal change is not changed therefore.
|
||||||
|
* See also dc_marknoticed_chat().
|
||||||
|
*
|
||||||
|
* Moreover, timer is started for incoming ephemeral messages.
|
||||||
|
* This also happens for messages in the deaddrop.
|
||||||
*
|
*
|
||||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||||
*
|
*
|
||||||
@@ -2980,7 +2990,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
|
|||||||
*
|
*
|
||||||
* @memberof dc_chat_t
|
* @memberof dc_chat_t
|
||||||
* @param chat The chat object.
|
* @param chat The chat object.
|
||||||
* @return Path and file if the profile image, if any.
|
* @return Path and file of the profile image, if any.
|
||||||
* NULL otherwise.
|
* NULL otherwise.
|
||||||
* Must be released using dc_str_unref() after usage.
|
* Must be released using dc_str_unref() after usage.
|
||||||
*/
|
*/
|
||||||
@@ -3065,7 +3075,7 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if messages can be sent to a give chat.
|
* Check if messages can be sent to a given chat.
|
||||||
* This is not true e.g. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
|
* This is not true e.g. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
|
||||||
*
|
*
|
||||||
* Calling dc_send_msg() for these chats will fail
|
* Calling dc_send_msg() for these chats will fail
|
||||||
@@ -5625,6 +5635,11 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||||
#define DC_STR_EPHEMERAL_WEEKS 96
|
#define DC_STR_EPHEMERAL_WEEKS 96
|
||||||
|
|
||||||
|
/// "Forwarded"
|
||||||
|
///
|
||||||
|
/// Used in message summary text for notifications and chatlist.
|
||||||
|
#define DC_STR_FORWARDED 97
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use std::ptr;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
use async_std::task::{block_on, spawn};
|
use async_std::task::{block_on, spawn};
|
||||||
use num_traits::{FromPrimitive, ToPrimitive};
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
|
|
||||||
@@ -130,12 +131,14 @@ pub unsafe extern "C" fn dc_set_config(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
match config::Config::from_str(&to_string_lossy(key)) {
|
let key = to_string_lossy(key);
|
||||||
// When ctx.set_config() fails it already logged the error.
|
match config::Config::from_str(&key) {
|
||||||
// TODO: Context::set_config() should not log this
|
|
||||||
Ok(key) => block_on(async move {
|
Ok(key) => block_on(async move {
|
||||||
ctx.set_config(key, to_opt_string_lossy(value).as_deref())
|
let value = to_opt_string_lossy(value);
|
||||||
|
ctx.set_config(key, value.as_deref())
|
||||||
.await
|
.await
|
||||||
|
.with_context(|| format!("Can't set {} to {:?}", key, value))
|
||||||
|
.log_err(ctx, "dc_set_config() failed")
|
||||||
.is_ok() as libc::c_int
|
.is_ok() as libc::c_int
|
||||||
}),
|
}),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -268,7 +271,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
|||||||
let redirect = to_string_lossy(redirect);
|
let redirect = to_string_lossy(redirect);
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
match oauth2::dc_get_oauth2_url(&ctx, addr, redirect).await {
|
match oauth2::dc_get_oauth2_url(&ctx, &addr, &redirect).await {
|
||||||
Some(res) => res.strdup(),
|
Some(res) => res.strdup(),
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
@@ -636,7 +639,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
chat::create_by_contact_id(&ctx, contact_id)
|
ChatId::create_for_contact(&ctx, contact_id)
|
||||||
.await
|
.await
|
||||||
.log_err(ctx, "Failed to create chat from contact_id")
|
.log_err(ctx, "Failed to create chat from contact_id")
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
@@ -656,11 +659,12 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
chat::get_by_contact_id(&ctx, contact_id)
|
ChatId::lookup_by_contact(&ctx, contact_id)
|
||||||
.await
|
.await
|
||||||
.log_err(ctx, "Failed to get chat for contact_id")
|
.log_err(ctx, "Failed to get chat for contact_id")
|
||||||
|
.unwrap_or_default() // unwraps the Result
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
.unwrap_or(0)
|
.unwrap_or(0) // unwraps the Option
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,7 +1188,7 @@ pub unsafe extern "C" fn dc_search_msgs(
|
|||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
let arr = dc_array_t::from(
|
let arr = dc_array_t::from(
|
||||||
ctx.search_msgs(chat_id, to_string_lossy(query))
|
ctx.search_msgs(chat_id, &to_string_lossy(query))
|
||||||
.await
|
.await
|
||||||
.unwrap_or_log_default(ctx, "Failed search_msgs")
|
.unwrap_or_log_default(ctx, "Failed search_msgs")
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1233,7 +1237,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
|||||||
};
|
};
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
chat::create_group_chat(&ctx, protect, to_string_lossy(name))
|
chat::create_group_chat(&ctx, protect, &to_string_lossy(name))
|
||||||
.await
|
.await
|
||||||
.log_err(ctx, "Failed to create group chat")
|
.log_err(ctx, "Failed to create group chat")
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
@@ -1308,7 +1312,7 @@ pub unsafe extern "C" fn dc_set_chat_name(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
chat::set_chat_name(&ctx, ChatId::new(chat_id), to_string_lossy(name))
|
chat::set_chat_name(&ctx, ChatId::new(chat_id), &to_string_lossy(name))
|
||||||
.await
|
.await
|
||||||
.map(|_| 1)
|
.map(|_| 1)
|
||||||
.unwrap_or_log_default(&ctx, "Failed to set chat name")
|
.unwrap_or_log_default(&ctx, "Failed to set chat name")
|
||||||
@@ -1505,8 +1509,7 @@ pub unsafe extern "C" fn dc_delete_msgs(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||||
|
|
||||||
block_on(message::delete_msgs(&ctx, &msg_ids));
|
block_on(message::delete_msgs(&ctx, &msg_ids))
|
||||||
info!(&ctx, "verbose (issue 2335): ffi called dc_delete_msgs()");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -1558,7 +1561,9 @@ pub unsafe extern "C" fn dc_markseen_msgs(
|
|||||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(message::markseen_msgs(&ctx, msg_ids));
|
block_on(message::markseen_msgs(&ctx, msg_ids))
|
||||||
|
.log_err(ctx, "failed dc_markseen_msgs() call")
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -1637,7 +1642,7 @@ pub unsafe extern "C" fn dc_create_contact(
|
|||||||
let name = to_string_lossy(name);
|
let name = to_string_lossy(name);
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
Contact::create(&ctx, name, to_string_lossy(addr))
|
Contact::create(&ctx, &name, &to_string_lossy(addr))
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
})
|
})
|
||||||
@@ -1655,7 +1660,7 @@ pub unsafe extern "C" fn dc_add_address_book(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
match Contact::add_address_book(&ctx, to_string_lossy(addr_book)).await {
|
match Contact::add_address_book(&ctx, &to_string_lossy(addr_book)).await {
|
||||||
Ok(cnt) => cnt as libc::c_int,
|
Ok(cnt) => cnt as libc::c_int,
|
||||||
Err(_) => 0,
|
Err(_) => 0,
|
||||||
}
|
}
|
||||||
@@ -1822,7 +1827,7 @@ pub unsafe extern "C" fn dc_imex(
|
|||||||
|
|
||||||
if let Some(param1) = to_opt_string_lossy(param1) {
|
if let Some(param1) = to_opt_string_lossy(param1) {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
imex::imex(&ctx, what, ¶m1)
|
imex::imex(&ctx, what, param1.as_ref())
|
||||||
.await
|
.await
|
||||||
.log_err(ctx, "IMEX failed")
|
.log_err(ctx, "IMEX failed")
|
||||||
});
|
});
|
||||||
@@ -1843,7 +1848,7 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
match imex::has_backup(&ctx, to_string_lossy(dir)).await {
|
match imex::has_backup(&ctx, to_string_lossy(dir).as_ref()).await {
|
||||||
Ok(res) => res.strdup(),
|
Ok(res) => res.strdup(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// do not bubble up error to the user,
|
// do not bubble up error to the user,
|
||||||
@@ -1924,7 +1929,7 @@ pub unsafe extern "C" fn dc_check_qr(
|
|||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
let lot = qr::check_qr(&ctx, to_string_lossy(qr)).await;
|
let lot = qr::check_qr(&ctx, &to_string_lossy(qr)).await;
|
||||||
Box::into_raw(Box::new(lot))
|
Box::into_raw(Box::new(lot))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
13
deltachat_derive/Cargo.toml
Normal file
13
deltachat_derive/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "deltachat_derive"
|
||||||
|
version = "2.0.0"
|
||||||
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = "1.0.72"
|
||||||
|
quote = "1.0.2"
|
||||||
47
deltachat_derive/src/lib.rs
Normal file
47
deltachat_derive/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#![recursion_limit = "128"]
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use crate::proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
// For now, assume (not check) that these macroses are applied to enum without
|
||||||
|
// data. If this assumption is violated, compiler error will point to
|
||||||
|
// generated code, which is not very user-friendly.
|
||||||
|
|
||||||
|
#[proc_macro_derive(ToSql)]
|
||||||
|
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||||
|
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||||
|
let name = &ast.ident;
|
||||||
|
|
||||||
|
let gen = quote! {
|
||||||
|
impl rusqlite::types::ToSql for #name {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let num = *self as i64;
|
||||||
|
let value = rusqlite::types::Value::Integer(num);
|
||||||
|
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
||||||
|
std::result::Result::Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gen.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(FromSql)]
|
||||||
|
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||||
|
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||||
|
let name = &ast.ident;
|
||||||
|
|
||||||
|
let gen = quote! {
|
||||||
|
impl rusqlite::types::FromSql for #name {
|
||||||
|
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||||
|
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||||
|
if let Some(value) = num_traits::FromPrimitive::from_i64(inner) {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(rusqlite::types::FromSqlError::OutOfRange(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
gen.into()
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
if 0 != bits & 1 {
|
if 0 != bits & 1 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM jobs;"))
|
.execute("DELETE FROM jobs;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("(1) Jobs reset.");
|
println!("(1) Jobs reset.");
|
||||||
@@ -42,7 +42,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
if 0 != bits & 2 {
|
if 0 != bits & 2 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM acpeerstates;"))
|
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("(2) Peerstates reset.");
|
println!("(2) Peerstates reset.");
|
||||||
@@ -50,7 +50,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
if 0 != bits & 4 {
|
if 0 != bits & 4 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM keypairs;"))
|
.execute("DELETE FROM keypairs;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("(4) Private keypairs reset.");
|
println!("(4) Private keypairs reset.");
|
||||||
@@ -58,34 +58,35 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
if 0 != bits & 8 {
|
if 0 != bits & 8 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM contacts WHERE id>9;"))
|
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM chats WHERE id>9;"))
|
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM chats_contacts;"))
|
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM msgs WHERE id>9;"))
|
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query(
|
.execute(
|
||||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||||
))
|
paramsv![],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
.execute(sqlx::query("DELETE FROM leftgrps;"))
|
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("(8) Rest but server config reset.");
|
println!("(8) Rest but server config reset.");
|
||||||
@@ -456,20 +457,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"export-backup" => {
|
"export-backup" => {
|
||||||
let dir = dirs::home_dir().unwrap_or_default();
|
let dir = dirs::home_dir().unwrap_or_default();
|
||||||
imex(&context, ImexMode::ExportBackup, &dir).await?;
|
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
|
||||||
println!("Exported to {}.", dir.to_string_lossy());
|
println!("Exported to {}.", dir.to_string_lossy());
|
||||||
}
|
}
|
||||||
"import-backup" => {
|
"import-backup" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||||
imex(&context, ImexMode::ImportBackup, arg1).await?;
|
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
|
||||||
}
|
}
|
||||||
"export-keys" => {
|
"export-keys" => {
|
||||||
let dir = dirs::home_dir().unwrap_or_default();
|
let dir = dirs::home_dir().unwrap_or_default();
|
||||||
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
|
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
|
||||||
println!("Exported to {}.", dir.to_string_lossy());
|
println!("Exported to {}.", dir.to_string_lossy());
|
||||||
}
|
}
|
||||||
"import-keys" => {
|
"import-keys" => {
|
||||||
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
|
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
|
||||||
}
|
}
|
||||||
"export-setup" => {
|
"export-setup" => {
|
||||||
let setup_code = create_setup_code(&context);
|
let setup_code = create_setup_code(&context);
|
||||||
@@ -603,7 +604,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let sel_chat = sel_chat.as_ref().unwrap();
|
let sel_chat = sel_chat.as_ref().unwrap();
|
||||||
|
|
||||||
let time_start = std::time::SystemTime::now();
|
let time_start = std::time::SystemTime::now();
|
||||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
|
let msglist =
|
||||||
|
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
|
||||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||||
|
|
||||||
let msglist: Vec<MsgId> = msglist
|
let msglist: Vec<MsgId> = msglist
|
||||||
@@ -673,7 +675,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"createchat" => {
|
"createchat" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||||
let contact_id: u32 = arg1.parse()?;
|
let contact_id: u32 = arg1.parse()?;
|
||||||
let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
|
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
|
||||||
|
|
||||||
println!("Single#{} created successfully.", chat_id,);
|
println!("Single#{} created successfully.", chat_id,);
|
||||||
}
|
}
|
||||||
@@ -1057,7 +1059,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
let mut msg_ids = vec![MsgId::new(0)];
|
let mut msg_ids = vec![MsgId::new(0)];
|
||||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||||
message::markseen_msgs(&context, msg_ids).await;
|
message::markseen_msgs(&context, msg_ids).await?;
|
||||||
}
|
}
|
||||||
"delmsg" => {
|
"delmsg" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
@@ -1084,7 +1086,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
if !arg2.is_empty() {
|
if !arg2.is_empty() {
|
||||||
let book = format!("{}\n{}", arg1, arg2);
|
let book = format!("{}\n{}", arg1, arg2);
|
||||||
Contact::add_address_book(&context, book).await?;
|
Contact::add_address_book(&context, &book).await?;
|
||||||
} else {
|
} else {
|
||||||
Contact::create(&context, "", arg1).await?;
|
Contact::create(&context, "", arg1).await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ use rustyline::config::OutputStreamType;
|
|||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||||
use rustyline::hint::{Hinter, HistoryHinter};
|
use rustyline::hint::{Hinter, HistoryHinter};
|
||||||
|
use rustyline::validate::Validator;
|
||||||
use rustyline::{
|
use rustyline::{
|
||||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyPress,
|
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod cmdline;
|
mod cmdline;
|
||||||
@@ -237,7 +238,9 @@ const MISC_COMMANDS: [&str; 10] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<String> {
|
type Hint = String;
|
||||||
|
|
||||||
|
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<Self::Hint> {
|
||||||
if !line.is_empty() {
|
if !line.is_empty() {
|
||||||
for &cmds in &[
|
for &cmds in &[
|
||||||
&IMEX_COMMANDS[..],
|
&IMEX_COMMANDS[..],
|
||||||
@@ -259,11 +262,10 @@ impl Hinter for DcHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
||||||
static PROMPT: &str = "> ";
|
|
||||||
|
|
||||||
impl Highlighter for DcHelper {
|
impl Highlighter for DcHelper {
|
||||||
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&self, prompt: &'p str, default: bool) -> Cow<'b, str> {
|
||||||
if prompt == PROMPT {
|
if default {
|
||||||
Borrowed(COLORED_PROMPT)
|
Borrowed(COLORED_PROMPT)
|
||||||
} else {
|
} else {
|
||||||
Borrowed(prompt)
|
Borrowed(prompt)
|
||||||
@@ -284,6 +286,7 @@ impl Highlighter for DcHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Helper for DcHelper {}
|
impl Helper for DcHelper {}
|
||||||
|
impl Validator for DcHelper {}
|
||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
@@ -317,8 +320,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
};
|
};
|
||||||
let mut rl = Editor::with_config(config);
|
let mut rl = Editor::with_config(config);
|
||||||
rl.set_helper(Some(h));
|
rl.set_helper(Some(h));
|
||||||
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
|
||||||
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
|
||||||
if rl.load_history(".dc-history.txt").is_err() {
|
if rl.load_history(".dc-history.txt").is_err() {
|
||||||
println!("No previous history.");
|
println!("No previous history.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
use deltachat::chat;
|
use deltachat::chat::{self, ChatId};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
@@ -70,7 +70,7 @@ async fn main() {
|
|||||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
|
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
|
||||||
|
|
||||||
for i in 0..1 {
|
for i in 0..1 {
|
||||||
log::info!("sending message {}", i);
|
log::info!("sending message {}", i);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def main():
|
|||||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
|
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||||
packages=setuptools.find_packages('src'),
|
packages=setuptools.find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
import types
|
import types
|
||||||
from os.path import abspath
|
|
||||||
from os.path import dirname as dn
|
|
||||||
|
|
||||||
import cffi
|
import cffi
|
||||||
|
|
||||||
@@ -50,6 +48,7 @@ def system_build_flags():
|
|||||||
flags.objs = []
|
flags.objs = []
|
||||||
flags.incs = []
|
flags.incs = []
|
||||||
flags.extra_link_args = []
|
flags.extra_link_args = []
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def extract_functions(flags):
|
def extract_functions(flags):
|
||||||
@@ -168,11 +167,8 @@ def extract_defines(flags):
|
|||||||
|
|
||||||
def ffibuilder():
|
def ffibuilder():
|
||||||
projdir = os.environ.get('DCC_RS_DEV')
|
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 projdir:
|
||||||
|
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||||
flags = local_build_flags(projdir, target)
|
flags = local_build_flags(projdir, target)
|
||||||
else:
|
else:
|
||||||
flags = system_build_flags()
|
flags = system_build_flags()
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from .capi import lib
|
|||||||
from .events import FFIEventLogger, FFIEventTracker
|
from .events import FFIEventLogger, FFIEventTracker
|
||||||
from _pytest._code import Source
|
from _pytest._code import Source
|
||||||
from deltachat import direct_imap
|
from deltachat import direct_imap
|
||||||
|
from deltachat.account import parseaddr
|
||||||
|
|
||||||
|
|
||||||
import deltachat
|
import deltachat
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@ def pytest_addoption(parser):
|
|||||||
"--strict-tls", action="store_true",
|
"--strict-tls", action="store_true",
|
||||||
help="Never accept invalid TLS certificates for test accounts",
|
help="Never accept invalid TLS certificates for test accounts",
|
||||||
)
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--provider-file", "-P", default=None,
|
||||||
|
help="file which contains config settings for real-world providers")
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
@@ -125,17 +130,30 @@ def pytest_report_header(config, startdir):
|
|||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def parse_accountfile(fn):
|
||||||
|
if fn is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for line in open(fn):
|
||||||
|
if line.strip() and not line.strip().startswith('#'):
|
||||||
|
config_dict = {}
|
||||||
|
for part in line.split():
|
||||||
|
name, value = part.split("=")
|
||||||
|
config_dict[name] = value
|
||||||
|
yield config_dict
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_generate_tests(metafunc):
|
||||||
|
if "real_provider_config" in metafunc.fixturenames:
|
||||||
|
account_configs = list(parse_accountfile(metafunc.config.getoption("--provider-file")))
|
||||||
|
ids = [parseaddr(cfg["addr"])[1] for cfg in account_configs]
|
||||||
|
metafunc.parametrize("real_provider_config", account_configs, ids=ids)
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromFile:
|
class SessionLiveConfigFromFile:
|
||||||
def __init__(self, fn):
|
def __init__(self, fn):
|
||||||
self.fn = fn
|
self.fn = fn
|
||||||
self.configlist = []
|
self.configlist = list(parse_accountfile(fn))
|
||||||
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):
|
def get(self, index):
|
||||||
return self.configlist[index]
|
return self.configlist[index]
|
||||||
@@ -232,6 +250,15 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
|||||||
acc.disable_logging()
|
acc.disable_logging()
|
||||||
deltachat.unregister_global_plugin(direct_imap)
|
deltachat.unregister_global_plugin(direct_imap)
|
||||||
|
|
||||||
|
def make_account_from_real_config(self, provider_config):
|
||||||
|
configdict = provider_config
|
||||||
|
addr = parseaddr(configdict["addr"])[1]
|
||||||
|
domain = addr.split("@")[1]
|
||||||
|
if "e2ee_enabled" not in configdict:
|
||||||
|
configdict["e2ee_enabled"] = "1"
|
||||||
|
tmpdb = tmpdir.join(domain)
|
||||||
|
return self.make_account(tmpdb.strpath, logid=domain)
|
||||||
|
|
||||||
def make_account(self, path, logid, quiet=False):
|
def make_account(self, path, logid, quiet=False):
|
||||||
ac = Account(path, logging=self._logging)
|
ac = Account(path, logging=self._logging)
|
||||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||||
@@ -361,6 +388,11 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
|||||||
accounts = self._accounts[:]
|
accounts = self._accounts[:]
|
||||||
started_accounts = []
|
started_accounts = []
|
||||||
for acc in accounts:
|
for acc in accounts:
|
||||||
|
if hasattr(acc, "_configtracker"):
|
||||||
|
acc._configtracker.wait_finish()
|
||||||
|
acc._evtracker.consume_events()
|
||||||
|
acc.get_device_chat().mark_noticed()
|
||||||
|
del acc._configtracker
|
||||||
if acc not in started_accounts:
|
if acc not in started_accounts:
|
||||||
self.wait_configure(acc)
|
self.wait_configure(acc)
|
||||||
acc.set_config("bcc_self", "0")
|
acc.set_config("bcc_self", "0")
|
||||||
@@ -369,6 +401,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
|||||||
started_accounts.append(acc)
|
started_accounts.append(acc)
|
||||||
print("{}: {} account was started".format(
|
print("{}: {} account was started".format(
|
||||||
acc.get_config("displayname"), acc.get_config("addr")))
|
acc.get_config("displayname"), acc.get_config("addr")))
|
||||||
|
|
||||||
for acc in started_accounts:
|
for acc in started_accounts:
|
||||||
acc._evtracker.wait_all_initial_fetches()
|
acc._evtracker.wait_all_initial_fetches()
|
||||||
|
|
||||||
|
|||||||
72
python/tests/real_providers.py
Normal file
72
python/tests/real_providers.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def acprovider(acfactory, real_provider_config):
|
||||||
|
ac = acfactory.make_account_from_real_config(real_provider_config)
|
||||||
|
ac.update_config(real_provider_config)
|
||||||
|
ac._configtracker = ac.configure()
|
||||||
|
return ac
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def actest(acfactory):
|
||||||
|
return acfactory.get_online_configuring_account()
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_success(acfactory, acprovider, lp):
|
||||||
|
lp.sec("waiting for successful configuration of provider account")
|
||||||
|
acfactory.wait_configure_and_start_io()
|
||||||
|
|
||||||
|
assert acprovider.is_configured()
|
||||||
|
for name in ("inbox", "mvbox", "sentbox"):
|
||||||
|
folder = acprovider.get_config("configured_" + name + "_folder")
|
||||||
|
if not folder:
|
||||||
|
lp.sec("found no {} folder".format(name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
lp.sec("removing provider account IMAP folder {}".format(folder))
|
||||||
|
acprovider.direct_imap.select_folder(folder)
|
||||||
|
acprovider.direct_imap.delete("1:*")
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_send_receive(acprovider, actest, acfactory, lp):
|
||||||
|
acfactory.wait_configure_and_start_io()
|
||||||
|
|
||||||
|
lp.sec("sending message from test account to provider account")
|
||||||
|
chat = actest.create_chat(acprovider)
|
||||||
|
chat.send_text("hello")
|
||||||
|
|
||||||
|
lp.sec("receiving message with the provider account")
|
||||||
|
msg = acprovider._evtracker.wait_next_messages_changed()
|
||||||
|
assert msg.chat.is_deaddrop() and not msg.is_encrypted()
|
||||||
|
|
||||||
|
lp.sec("sending message back from provider to test account")
|
||||||
|
back_chat = acprovider.create_chat(actest)
|
||||||
|
back_chat.send_text("world")
|
||||||
|
|
||||||
|
lp.sec("waiting with test account for provider mail")
|
||||||
|
msg = actest._evtracker.wait_next_incoming_message()
|
||||||
|
assert msg.text == "world"
|
||||||
|
assert msg.is_encrypted()
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_messages(acprovider, actest, acfactory, lp):
|
||||||
|
acfactory.wait_configure_and_start_io()
|
||||||
|
|
||||||
|
lp.sec("sending message from test account to provider account")
|
||||||
|
chat = actest.create_chat(acprovider)
|
||||||
|
chat.send_text("hello")
|
||||||
|
|
||||||
|
lp.sec("receiving message with the provider account")
|
||||||
|
msg = acprovider._evtracker.wait_next_messages_changed()
|
||||||
|
assert msg.chat.is_deaddrop() and not msg.is_encrypted()
|
||||||
|
|
||||||
|
lp.sec("sending message back from provider to test account")
|
||||||
|
back_chat = acprovider.create_chat(actest)
|
||||||
|
back_chat.send_text("world")
|
||||||
|
|
||||||
|
lp.sec("waiting with test account for provider mail")
|
||||||
|
msg = actest._evtracker.wait_next_incoming_message()
|
||||||
|
assert msg.text == "world"
|
||||||
|
assert msg.is_encrypted()
|
||||||
@@ -1713,7 +1713,7 @@ class TestOnlineAccount:
|
|||||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||||
|
|
||||||
def test_qr_verified_group_and_chatting(self, acfactory, lp):
|
def test_qr_verified_group_and_chatting(self, acfactory, lp):
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||||
assert chat1.is_protected()
|
assert chat1.is_protected()
|
||||||
@@ -1744,6 +1744,29 @@ class TestOnlineAccount:
|
|||||||
assert msg.text == "world"
|
assert msg.text == "world"
|
||||||
assert msg.is_encrypted()
|
assert msg.is_encrypted()
|
||||||
|
|
||||||
|
lp.sec("ac1: create QR code and let ac3 scan it, starting the securejoin")
|
||||||
|
qr = ac1.get_setup_contact_qr()
|
||||||
|
|
||||||
|
lp.sec("ac3: start QR-code based setup contact protocol")
|
||||||
|
ch = ac3.qr_setup_contact(qr)
|
||||||
|
assert ch.id >= 10
|
||||||
|
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||||
|
|
||||||
|
lp.sec("ac1: add ac3 to verified group")
|
||||||
|
chat1.add_contact(ac3)
|
||||||
|
msg = ac2._evtracker.wait_next_incoming_message()
|
||||||
|
assert msg.is_encrypted()
|
||||||
|
assert msg.is_system_message()
|
||||||
|
assert not msg.error
|
||||||
|
|
||||||
|
lp.sec("ac2: send message and let ac3 read it")
|
||||||
|
chat2.send_text("hi")
|
||||||
|
# Skip system message about added member
|
||||||
|
ac3._evtracker.wait_next_incoming_message()
|
||||||
|
msg = ac3._evtracker.wait_next_incoming_message()
|
||||||
|
assert msg.text == "hi"
|
||||||
|
assert msg.is_encrypted()
|
||||||
|
|
||||||
def test_set_get_contact_avatar(self, acfactory, data, lp):
|
def test_set_get_contact_avatar(self, acfactory, data, lp):
|
||||||
lp.sec("configuring ac1 and ac2")
|
lp.sec("configuring ac1 and ac2")
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||||
@@ -1947,6 +1970,47 @@ class TestOnlineAccount:
|
|||||||
assert msg_back.chat == chat
|
assert msg_back.chat == chat
|
||||||
assert chat.get_profile_image() is None
|
assert chat.get_profile_image() is None
|
||||||
|
|
||||||
|
def test_fetch_deleted_msg(self, acfactory, lp):
|
||||||
|
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
|
||||||
|
hundreds of times, because uid_next was not updated.
|
||||||
|
|
||||||
|
See https://github.com/deltachat/deltachat-core-rust/issues/2429.
|
||||||
|
"""
|
||||||
|
ac1 = acfactory.get_one_online_account()
|
||||||
|
ac1.stop_io()
|
||||||
|
|
||||||
|
ac1.direct_imap.append("INBOX", """
|
||||||
|
From: alice <alice@example.org>
|
||||||
|
Subject: subj
|
||||||
|
To: bob@example.com
|
||||||
|
Chat-Version: 1.0
|
||||||
|
Message-ID: <aepiors@example.org>
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
Deleted message
|
||||||
|
""")
|
||||||
|
ac1.direct_imap.delete("1:*", expunge=False)
|
||||||
|
ac1.start_io()
|
||||||
|
|
||||||
|
for ev in ac1._evtracker.iter_events():
|
||||||
|
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||||
|
pytest.fail("A deleted message was shown to the user")
|
||||||
|
|
||||||
|
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
|
||||||
|
break
|
||||||
|
|
||||||
|
# The message was downloaded once, now check that it's not downloaded again
|
||||||
|
|
||||||
|
for ev in ac1._evtracker.iter_events():
|
||||||
|
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
|
||||||
|
pytest.fail("The same email was read twice")
|
||||||
|
|
||||||
|
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||||
|
pytest.fail("A deleted message was shown to the user")
|
||||||
|
|
||||||
|
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2:
|
||||||
|
break # DC is done with reading messages
|
||||||
|
|
||||||
def test_send_receive_locations(self, acfactory, lp):
|
def test_send_receive_locations(self, acfactory, lp):
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[tox]
|
[tox]
|
||||||
# make sure to update environment list in travis.yml and appveyor.yml
|
|
||||||
envlist =
|
envlist =
|
||||||
py37
|
py3
|
||||||
lint
|
lint
|
||||||
auditwheels
|
auditwheels
|
||||||
|
|
||||||
@@ -46,10 +45,9 @@ commands =
|
|||||||
[testenv:doc]
|
[testenv:doc]
|
||||||
changedir=doc
|
changedir=doc
|
||||||
deps =
|
deps =
|
||||||
# With Python 3.7 and Sphinx 3.5.0, it throws an exception.
|
# Pin dependencies to the versions which actually work with Python 3.5.
|
||||||
# Pin the version to the working one.
|
|
||||||
sphinx==3.4.3
|
sphinx==3.4.3
|
||||||
breathe
|
breathe==4.28.0
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM quay.io/pypa/manylinux2010_x86_64
|
FROM quay.io/pypa/manylinux2014_x86_64
|
||||||
|
|
||||||
# Configure ld.so/ldconfig and pkg-config
|
# Configure ld.so/ldconfig and pkg-config
|
||||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -xe
|
set -xe
|
||||||
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
|
|
||||||
export CIRCLE_BUILD_NUM=$USER
|
|
||||||
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
|
|
||||||
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
|
|
||||||
|
|
||||||
time bash scripts/$CIRCLE_JOB.sh
|
JOB=${1:?need to specify 'rust' or 'python'}
|
||||||
|
BRANCH="$(git branch | grep \* | cut -d ' ' -f2)"
|
||||||
|
REPONAME="$(basename $(git rev-parse --show-toplevel))"
|
||||||
|
|
||||||
|
time bash "scripts/remote_tests_$JOB.sh" "$USER-$BRANCH-$REPONAME"
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -Dwarnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
name: Build and test
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
|
||||||
rust: [nightly]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.rust }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.rust == 'nightly'
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all
|
|
||||||
|
|
||||||
- name: tests ignored
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --release -- --ignored
|
|
||||||
|
|
||||||
check_fmt:
|
|
||||||
name: Checking fmt and docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- name: fmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
# clippy_check:
|
|
||||||
# name: Clippy check
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
#
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v1
|
|
||||||
# - uses: actions-rs/toolchain@v1
|
|
||||||
# with:
|
|
||||||
# profile: minimal
|
|
||||||
# toolchain: nightly
|
|
||||||
# override: true
|
|
||||||
# components: clippy
|
|
||||||
#
|
|
||||||
# - name: clippy
|
|
||||||
# run: cargo clippy --all
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Build the Delta Chat C/Rust library typically run in a docker
|
|
||||||
# container that contains all library deps but should also work
|
|
||||||
# outside if you have the dependencies installed on your system.
|
|
||||||
|
|
||||||
set -e -x
|
|
||||||
|
|
||||||
# Perform clean build of core and install.
|
|
||||||
export TOXWORKDIR=.docker-tox
|
|
||||||
|
|
||||||
# install core lib
|
|
||||||
|
|
||||||
export PATH=/root/.cargo/bin:$PATH
|
|
||||||
cargo build --release -p deltachat_ffi
|
|
||||||
# cargo test --all --all-features
|
|
||||||
|
|
||||||
# Statically link against libdeltachat.a.
|
|
||||||
export DCC_RS_DEV=$(pwd)
|
|
||||||
|
|
||||||
# Configure access to a base python and to several python interpreters
|
|
||||||
# needed by tox below.
|
|
||||||
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
pushd /bin
|
|
||||||
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
|
|
||||||
popd
|
|
||||||
|
|
||||||
if [ -n "$TESTS" ]; then
|
|
||||||
|
|
||||||
pushd python
|
|
||||||
# prepare a clean tox run
|
|
||||||
rm -rf tests/__pycache__
|
|
||||||
rm -rf src/deltachat/__pycache__
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
|
|
||||||
# allows running of "liveconfig" tests but for speed reasons
|
|
||||||
# we run them only for the highest python version we support
|
|
||||||
|
|
||||||
# we split out qr-tests run to minimize likelyness of flaky tests
|
|
||||||
# (some qr tests are pretty heavy in terms of send/received
|
|
||||||
# messages and rust's imap code likely has concurrency problems)
|
|
||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
|
||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
|
||||||
unset DCC_NEW_TMP_EMAIL
|
|
||||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
|
||||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# if [ -n "$DOCS" ]; then
|
|
||||||
# echo -----------------------
|
|
||||||
# echo generating python docs
|
|
||||||
# echo -----------------------
|
|
||||||
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
|
||||||
# fi
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
BUILD_ID=${1:?specify build ID}
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||||
|
BUILDDIR=ci_builds/$BUILD_ID
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|||||||
|
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
echo "--- Running Python tests remotely"
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
ssh $SSHTARGET <<_HERE
|
||||||
set +x -e
|
set +x -e
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:-master}
|
BUILD_ID=${1:?specify build ID}
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||||
|
BUILDDIR=ci_builds/$BUILD_ID
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||||
git ls-files >.rsynclist
|
git ls-files >.rsynclist
|
||||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
echo "--- Running Rust tests remotely"
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
ssh $SSHTARGET <<_HERE
|
||||||
set +x -e
|
set +x -e
|
||||||
|
|||||||
5
scripts/set_core_version.py
Executable file → Normal file
5
scripts/set_core_version.py
Executable file → Normal file
@@ -82,9 +82,10 @@ def main():
|
|||||||
subprocess.call(["git", "add", "-u"])
|
subprocess.call(["git", "add", "-u"])
|
||||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||||
|
|
||||||
print("after commit make sure to: ")
|
print("after commit, on master make sure to: ")
|
||||||
print("")
|
print("")
|
||||||
print(" git tag {}".format(newversion))
|
print(" git tag -a {}".format(newversion))
|
||||||
|
print(" git push origin {}".format(newversion))
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
318
src/blob.rs
318
src/blob.rs
@@ -1,5 +1,6 @@
|
|||||||
//! # Blob directory management
|
//! # Blob directory management
|
||||||
|
|
||||||
|
use core::cmp::max;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
@@ -7,8 +8,12 @@ use async_std::path::{Path, PathBuf};
|
|||||||
use async_std::prelude::*;
|
use async_std::prelude::*;
|
||||||
use async_std::{fs, io};
|
use async_std::{fs, io};
|
||||||
|
|
||||||
|
use anyhow::format_err;
|
||||||
|
use anyhow::Context as _;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use image::DynamicImage;
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
|
use image::ImageFormat;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -53,11 +58,11 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// underlying error.
|
/// underlying error.
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
context: &'a Context,
|
context: &'a Context,
|
||||||
suggested_name: impl AsRef<str>,
|
suggested_name: &str,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
let blobdir = context.get_blobdir();
|
let blobdir = context.get_blobdir();
|
||||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
|
||||||
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
|
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
|
||||||
file.write_all(data)
|
file.write_all(data)
|
||||||
.await
|
.await
|
||||||
@@ -132,18 +137,17 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// copied.
|
/// copied.
|
||||||
pub async fn create_and_copy(
|
pub async fn create_and_copy(
|
||||||
context: &'a Context,
|
context: &'a Context,
|
||||||
src: impl AsRef<Path>,
|
src: &Path,
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
let mut src_file =
|
let mut src_file = fs::File::open(src)
|
||||||
fs::File::open(src.as_ref())
|
.await
|
||||||
.await
|
.map_err(|err| BlobError::CopyFailure {
|
||||||
.map_err(|err| BlobError::CopyFailure {
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobname: String::from(""),
|
||||||
blobname: String::from(""),
|
src: src.to_path_buf(),
|
||||||
src: src.as_ref().to_path_buf(),
|
cause: err,
|
||||||
cause: err,
|
})?;
|
||||||
})?;
|
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
|
||||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
|
||||||
let (name, mut dst_file) =
|
let (name, mut dst_file) =
|
||||||
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
||||||
let name_for_err = name.clone();
|
let name_for_err = name.clone();
|
||||||
@@ -156,7 +160,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
return Err(BlobError::CopyFailure {
|
return Err(BlobError::CopyFailure {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobname: name_for_err,
|
blobname: name_for_err,
|
||||||
src: src.as_ref().to_path_buf(),
|
src: src.to_path_buf(),
|
||||||
cause: err,
|
cause: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,16 +194,13 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// the [BlobObject::from_path] methods. See those for possible
|
/// the [BlobObject::from_path] methods. See those for possible
|
||||||
/// errors.
|
/// errors.
|
||||||
pub async fn new_from_path(
|
pub async fn new_from_path(
|
||||||
context: &Context,
|
context: &'a Context,
|
||||||
src: impl AsRef<Path>,
|
src: &Path,
|
||||||
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
if src.starts_with(context.get_blobdir()) {
|
||||||
BlobObject::from_path(context, src)
|
BlobObject::from_path(context, src)
|
||||||
} else if src.as_ref().starts_with("$BLOBDIR/") {
|
} else if src.starts_with("$BLOBDIR/") {
|
||||||
BlobObject::from_name(
|
BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string())
|
||||||
context,
|
|
||||||
src.as_ref().to_str().unwrap_or_default().to_string(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
BlobObject::create_and_copy(context, src).await
|
BlobObject::create_and_copy(context, src).await
|
||||||
}
|
}
|
||||||
@@ -220,23 +221,22 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// [BlobError::WrongName] is used if the file name does not
|
/// [BlobError::WrongName] is used if the file name does not
|
||||||
/// remain identical after sanitisation.
|
/// remain identical after sanitisation.
|
||||||
pub fn from_path(
|
pub fn from_path(
|
||||||
context: &Context,
|
context: &'a Context,
|
||||||
path: impl AsRef<Path>,
|
path: &Path,
|
||||||
) -> std::result::Result<BlobObject, BlobError> {
|
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||||
let rel_path = path
|
let rel_path =
|
||||||
.as_ref()
|
path.strip_prefix(context.get_blobdir())
|
||||||
.strip_prefix(context.get_blobdir())
|
.map_err(|_| BlobError::WrongBlobdir {
|
||||||
.map_err(|_| BlobError::WrongBlobdir {
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
src: path.to_path_buf(),
|
||||||
src: path.as_ref().to_path_buf(),
|
})?;
|
||||||
})?;
|
if !BlobObject::is_acceptible_blob_name(rel_path) {
|
||||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
|
||||||
return Err(BlobError::WrongName {
|
return Err(BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.to_path_buf(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.to_path_buf(),
|
||||||
})?;
|
})?;
|
||||||
BlobObject::from_name(context, name.to_string())
|
BlobObject::from_name(context, name.to_string())
|
||||||
}
|
}
|
||||||
@@ -380,7 +380,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
|
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<(), BlobError> {
|
||||||
let blob_abs = self.to_abs_path();
|
let blob_abs = self.to_abs_path();
|
||||||
|
|
||||||
let img_wh =
|
let img_wh =
|
||||||
@@ -391,7 +391,15 @@ impl<'a> BlobObject<'a> {
|
|||||||
MediaQuality::Worse => WORSE_AVATAR_SIZE,
|
MediaQuality::Worse => WORSE_AVATAR_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.recode_to_size(context, blob_abs, img_wh).await
|
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||||
|
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||||
|
if let Some(new_name) = self
|
||||||
|
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
self.name = new_name;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
|
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
|
||||||
@@ -410,30 +418,69 @@ impl<'a> BlobObject<'a> {
|
|||||||
MediaQuality::Worse => WORSE_IMAGE_SIZE,
|
MediaQuality::Worse => WORSE_IMAGE_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.recode_to_size(context, blob_abs, img_wh).await
|
if self
|
||||||
|
.recode_to_size(context, blob_abs, img_wh, None)
|
||||||
|
.await?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(format_err!(
|
||||||
|
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recode_to_size(
|
async fn recode_to_size(
|
||||||
&self,
|
&self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
blob_abs: PathBuf,
|
mut blob_abs: PathBuf,
|
||||||
img_wh: u32,
|
mut img_wh: u32,
|
||||||
) -> Result<(), BlobError> {
|
max_bytes: Option<usize>,
|
||||||
|
) -> Result<Option<String>, BlobError> {
|
||||||
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
|
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||||
cause: err,
|
cause: err,
|
||||||
})?;
|
})?;
|
||||||
let orientation = self.get_exif_orientation(context);
|
let orientation = self.get_exif_orientation(context);
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
let mut changed_name = None;
|
||||||
|
|
||||||
let do_scale = img.width() > img_wh || img.height() > img_wh;
|
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||||
|
encoded.clear();
|
||||||
|
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn encode_img_exceeds_bytes(
|
||||||
|
context: &Context,
|
||||||
|
img: &DynamicImage,
|
||||||
|
max_bytes: Option<usize>,
|
||||||
|
encoded: &mut Vec<u8>,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
if let Some(max_bytes) = max_bytes {
|
||||||
|
encode_img(img, encoded)?;
|
||||||
|
if encoded.len() > max_bytes {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
|
||||||
|
encoded.len(),
|
||||||
|
img.width(),
|
||||||
|
img.height(),
|
||||||
|
max_bytes,
|
||||||
|
);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
|
||||||
|
|
||||||
|
let do_scale =
|
||||||
|
exceeds_width || encode_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||||
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
||||||
|
|
||||||
if do_scale || do_rotate {
|
if do_scale || do_rotate {
|
||||||
if do_scale {
|
|
||||||
img = img.thumbnail(img_wh, img_wh);
|
|
||||||
}
|
|
||||||
|
|
||||||
if do_rotate {
|
if do_rotate {
|
||||||
img = match orientation {
|
img = match orientation {
|
||||||
Ok(90) => img.rotate90(),
|
Ok(90) => img.rotate90(),
|
||||||
@@ -443,14 +490,60 @@ impl<'a> BlobObject<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
|
if do_scale {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
if !exceeds_width {
|
||||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
// The image is already smaller than img_wh, but exceeds max_bytes
|
||||||
cause: err.into(),
|
// We can directly start with trying to scale down to 2/3 of its current width
|
||||||
})?;
|
img_wh = max(img.width(), img.height()) * 2 / 3
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let new_img = img.thumbnail(img_wh, img_wh);
|
||||||
|
|
||||||
|
if encode_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||||
|
if img_wh < 20 {
|
||||||
|
return Err(format_err!(
|
||||||
|
"Failed to scale image to below {}B",
|
||||||
|
max_bytes.unwrap_or_default()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
img_wh = img_wh * 2 / 3;
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"Final scaled-down image size: {}B ({}px)",
|
||||||
|
encoded.len(),
|
||||||
|
img_wh
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The file format is JPEG now, we may have to change the file extension
|
||||||
|
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
|
||||||
|
blob_abs = blob_abs.with_extension("jpg");
|
||||||
|
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
|
||||||
|
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
|
||||||
|
changed_name = Some(format!("$BLOBDIR/{}", file_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded.is_empty() {
|
||||||
|
encode_img(&img, &mut encoded)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&blob_abs, &encoded)
|
||||||
|
.await
|
||||||
|
.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(())
|
Ok(changed_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
|
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
|
||||||
@@ -514,14 +607,14 @@ pub enum BlobError {
|
|||||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||||
#[error("Blob has a badname {}", .blobname.display())]
|
#[error("Blob has a badname {}", .blobname.display())]
|
||||||
WrongName { blobname: PathBuf },
|
WrongName { blobname: PathBuf },
|
||||||
#[error("Sql: {0}")]
|
|
||||||
Sql(#[from] crate::sql::Error),
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use fs::File;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::TestContext;
|
||||||
@@ -626,13 +719,15 @@ mod tests {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let src = t.dir.path().join("src");
|
let src = t.dir.path().join("src");
|
||||||
fs::write(&src, b"boo").await.unwrap();
|
fs::write(&src, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::create_and_copy(&t, &src).await.unwrap();
|
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
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()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
let whoops = t.dir.path().join("whoops");
|
let whoops = t.dir.path().join("whoops");
|
||||||
assert!(BlobObject::create_and_copy(&t, &whoops).await.is_err());
|
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
let whoops = t.get_blobdir().join("whoops");
|
let whoops = t.get_blobdir().join("whoops");
|
||||||
assert!(!whoops.exists().await);
|
assert!(!whoops.exists().await);
|
||||||
}
|
}
|
||||||
@@ -643,7 +738,9 @@ mod tests {
|
|||||||
|
|
||||||
let src_ext = t.dir.path().join("external");
|
let src_ext = t.dir.path().join("external");
|
||||||
fs::write(&src_ext, b"boo").await.unwrap();
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
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()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
@@ -660,7 +757,9 @@ mod tests {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||||
fs::write(&src_ext, b"boo").await.unwrap();
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blob.as_name(),
|
blob.as_name(),
|
||||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||||
@@ -715,4 +814,105 @@ mod tests {
|
|||||||
assert!(!stem.contains('*'));
|
assert!(!stem.contains('*'));
|
||||||
assert!(!stem.contains('?'));
|
assert!(!stem.contains('?'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_selfavatar_outside_blobdir() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||||
|
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||||
|
File::create(&avatar_src)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.write_all(avatar_bytes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let avatar_blob = t.get_blobdir().join("avatar.jpg");
|
||||||
|
assert!(!avatar_blob.exists().await);
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(avatar_blob.exists().await);
|
||||||
|
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
|
||||||
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||||
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
|
|
||||||
|
let img = image::open(avatar_src).unwrap();
|
||||||
|
assert_eq!(img.width(), 1000);
|
||||||
|
assert_eq!(img.height(), 1000);
|
||||||
|
|
||||||
|
let img = image::open(&avatar_blob).unwrap();
|
||||||
|
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
||||||
|
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
||||||
|
|
||||||
|
async fn file_size(path_buf: &PathBuf) -> u64 {
|
||||||
|
let file = File::open(path_buf).await.unwrap();
|
||||||
|
file.metadata().await.unwrap().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||||
|
|
||||||
|
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(file_size(&avatar_blob).await <= 3000);
|
||||||
|
assert!(file_size(&avatar_blob).await > 2000);
|
||||||
|
let img = image::open(&avatar_blob).unwrap();
|
||||||
|
assert!(img.width() > 130);
|
||||||
|
assert_eq!(img.width(), img.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_selfavatar_in_blobdir() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||||
|
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||||
|
File::create(&avatar_src)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.write_all(avatar_bytes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let img = image::open(&avatar_src).unwrap();
|
||||||
|
assert_eq!(img.width(), 900);
|
||||||
|
assert_eq!(img.height(), 900);
|
||||||
|
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
avatar_cfg,
|
||||||
|
avatar_src.with_extension("jpg").to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let img = image::open(avatar_cfg).unwrap();
|
||||||
|
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
||||||
|
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_selfavatar_copy_without_recode() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
|
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
|
File::create(&avatar_src)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.write_all(avatar_bytes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let avatar_blob = t.get_blobdir().join("avatar.png");
|
||||||
|
assert!(!avatar_blob.exists().await);
|
||||||
|
t.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.get_config(Config::Selfavatar).await.unwrap();
|
||||||
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1611
src/chat.rs
1611
src/chat.rs
File diff suppressed because it is too large
Load Diff
122
src/chatlist.rs
122
src/chatlist.rs
@@ -1,10 +1,7 @@
|
|||||||
//! # Chat list module
|
//! # Chat list module
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use async_std::prelude::*;
|
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::chat;
|
|
||||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
|
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
|
||||||
@@ -112,20 +109,23 @@ impl Chatlist {
|
|||||||
|
|
||||||
let mut add_archived_link_item = false;
|
let mut add_archived_link_item = false;
|
||||||
|
|
||||||
let skip_id = if flag_for_forwarding {
|
let process_row = |row: &rusqlite::Row| {
|
||||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
let chat_id: ChatId = row.get(0)?;
|
||||||
.await
|
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
||||||
.unwrap_or_default()
|
Ok((chat_id, msg_id))
|
||||||
.0
|
|
||||||
} else {
|
|
||||||
ChatId::new(0)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let process_row = |row: sqlx::Result<sqlx::sqlite::SqliteRow>| {
|
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||||
let row = row?;
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
let chat_id: ChatId = row.try_get(0)?;
|
.map_err(Into::into)
|
||||||
let msg_id: MsgId = row.try_get(1).unwrap_or_default();
|
};
|
||||||
Ok((chat_id, msg_id))
|
|
||||||
|
let skip_id = if flag_for_forwarding {
|
||||||
|
ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
ChatId::new(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// select with left join and minimum:
|
// select with left join and minimum:
|
||||||
@@ -143,10 +143,10 @@ impl Chatlist {
|
|||||||
// tg do the same) for the deaddrop, however, they should
|
// tg do the same) for the deaddrop, however, they should
|
||||||
// really be hidden, however, _currently_ the deaddrop is not
|
// really be hidden, however, _currently_ the deaddrop is not
|
||||||
// shown at all permanent in the chatlist.
|
// shown at all permanent in the chatlist.
|
||||||
let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id {
|
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||||
// show chats shared with a given contact
|
// show chats shared with a given contact
|
||||||
context.sql.fetch(
|
context.sql.query_map(
|
||||||
sqlx::query("SELECT c.id, m.id
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
@@ -160,9 +160,11 @@ impl Chatlist {
|
|||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
|
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned)
|
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||||
).await?.map(process_row).collect::<sqlx::Result<_>>().await?
|
process_row,
|
||||||
|
process_rows,
|
||||||
|
).await?
|
||||||
} else if flag_archived_only {
|
} else if flag_archived_only {
|
||||||
// show archived chats
|
// show archived chats
|
||||||
// (this includes the archived device-chat; we could skip it,
|
// (this includes the archived device-chat; we could skip it,
|
||||||
@@ -170,9 +172,8 @@ impl Chatlist {
|
|||||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT c.id, m.id
|
||||||
"SELECT c.id, m.id
|
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
@@ -187,13 +188,11 @@ impl Chatlist {
|
|||||||
AND c.archived=1
|
AND c.archived=1
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
)
|
paramsv![MessageState::OutDraft],
|
||||||
.bind(MessageState::OutDraft),
|
process_row,
|
||||||
|
process_rows,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.map(process_row)
|
|
||||||
.collect::<sqlx::Result<_>>()
|
|
||||||
.await?
|
|
||||||
} else if let Some(query) = query {
|
} else if let Some(query) = query {
|
||||||
let query = query.trim().to_string();
|
let query = query.trim().to_string();
|
||||||
ensure!(!query.is_empty(), "missing query");
|
ensure!(!query.is_empty(), "missing query");
|
||||||
@@ -207,9 +206,8 @@ impl Chatlist {
|
|||||||
let str_like_cmd = format!("%{}%", query);
|
let str_like_cmd = format!("%{}%", query);
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT c.id, m.id
|
||||||
"SELECT c.id, m.id
|
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
ON c.id=m.chat_id
|
ON c.id=m.chat_id
|
||||||
@@ -224,27 +222,21 @@ impl Chatlist {
|
|||||||
AND c.name LIKE ?3
|
AND c.name LIKE ?3
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||||
)
|
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||||
.bind(MessageState::OutDraft)
|
process_row,
|
||||||
.bind(skip_id)
|
process_rows,
|
||||||
.bind(str_like_cmd),
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.map(process_row)
|
|
||||||
.collect::<sqlx::Result<_>>()
|
|
||||||
.await?
|
|
||||||
} else {
|
} else {
|
||||||
// show normal chatlist
|
// show normal chatlist
|
||||||
let sort_id_up = if flag_for_forwarding {
|
let sort_id_up = if flag_for_forwarding {
|
||||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
|
||||||
.await
|
.await?
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.0
|
|
||||||
} else {
|
} else {
|
||||||
ChatId::new(0)
|
ChatId::new(0)
|
||||||
};
|
};
|
||||||
|
let mut ids = context.sql.query_map(
|
||||||
let mut ids: Vec<_> = context.sql.fetch(sqlx::query(
|
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id
|
||||||
FROM chats c
|
FROM chats c
|
||||||
LEFT JOIN msgs m
|
LEFT JOIN msgs m
|
||||||
@@ -259,15 +251,11 @@ impl Chatlist {
|
|||||||
AND c.blocked=0
|
AND c.blocked=0
|
||||||
AND NOT c.archived=?3
|
AND NOT c.archived=?3
|
||||||
GROUP BY c.id
|
GROUP BY c.id
|
||||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
|
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],
|
||||||
.bind(MessageState::OutDraft)
|
process_row,
|
||||||
.bind(skip_id)
|
process_rows,
|
||||||
.bind(ChatVisibility::Archived)
|
).await?;
|
||||||
.bind(sort_id_up)
|
|
||||||
.bind(ChatVisibility::Pinned)
|
|
||||||
).await?.map(process_row).collect::<sqlx::Result<_>>().await?;
|
|
||||||
|
|
||||||
if !flag_no_specials {
|
if !flag_no_specials {
|
||||||
if let Some(last_deaddrop_fresh_msg_id) =
|
if let Some(last_deaddrop_fresh_msg_id) =
|
||||||
get_last_deaddrop_fresh_msg(context).await?
|
get_last_deaddrop_fresh_msg(context).await?
|
||||||
@@ -410,9 +398,10 @@ impl Chatlist {
|
|||||||
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||||
let count = context
|
let count = context
|
||||||
.sql
|
.sql
|
||||||
.count(sqlx::query(
|
.count(
|
||||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||||
))
|
paramsv![],
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
@@ -422,16 +411,19 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>>
|
|||||||
// sufficient as there are typically only few fresh messages.
|
// sufficient as there are typically only few fresh messages.
|
||||||
let id = context
|
let id = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(sqlx::query(concat!(
|
.query_get_value(
|
||||||
"SELECT m.id",
|
concat!(
|
||||||
" FROM msgs m",
|
"SELECT m.id",
|
||||||
" LEFT JOIN chats c",
|
" FROM msgs m",
|
||||||
" ON c.id=m.chat_id",
|
" LEFT JOIN chats c",
|
||||||
" WHERE m.state=10",
|
" ON c.id=m.chat_id",
|
||||||
" AND m.hidden=0",
|
" WHERE m.state=10",
|
||||||
" AND c.blocked=2",
|
" AND m.hidden=0",
|
||||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
" AND c.blocked=2",
|
||||||
)))
|
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||||
|
),
|
||||||
|
paramsv![],
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use hsluv::hsluv_to_rgb;
|
|||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
|
|
||||||
/// Converts an identifier to Hue angle.
|
/// Converts an identifier to Hue angle.
|
||||||
fn str_to_angle(s: impl AsRef<str>) -> f64 {
|
fn str_to_angle(s: &str) -> f64 {
|
||||||
let bytes = s.as_ref().as_bytes();
|
let bytes = s.as_bytes();
|
||||||
let result = Sha1::digest(bytes);
|
let result = Sha1::digest(bytes);
|
||||||
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
|
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
|
||||||
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
||||||
@@ -31,7 +31,7 @@ fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
|
|||||||
///
|
///
|
||||||
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
|
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
|
||||||
/// half (50.0) to make colors suitable both for light and dark theme.
|
/// half (50.0) to make colors suitable both for light and dark theme.
|
||||||
pub(crate) fn str_to_color(s: impl AsRef<str>) -> u32 {
|
pub(crate) fn str_to_color(s: &str) -> u32 {
|
||||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,14 +242,14 @@ impl Context {
|
|||||||
match key {
|
match key {
|
||||||
Config::Selfavatar => {
|
Config::Selfavatar => {
|
||||||
self.sql
|
self.sql
|
||||||
.execute(sqlx::query("UPDATE contacts SET selfavatar_sent=0;"))
|
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
self.sql
|
self.sql
|
||||||
.set_raw_config_bool("attach_selfavatar", true)
|
.set_raw_config_bool("attach_selfavatar", true)
|
||||||
.await?;
|
.await?;
|
||||||
match value {
|
match value {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let blob = BlobObject::new_from_path(self, value).await?;
|
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
||||||
blob.recode_to_avatar_size(self).await?;
|
blob.recode_to_avatar_size(self).await?;
|
||||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -305,7 +305,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
|
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||||
self.set_config(key, if value { Some("1") } else { None })
|
self.set_config(key, if value { Some("1") } else { None })
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -331,12 +331,8 @@ mod tests {
|
|||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::constants::BALANCED_AVATAR_SIZE;
|
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::TestContext;
|
||||||
use image::GenericImageView;
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_string() {
|
fn test_to_string() {
|
||||||
@@ -350,82 +346,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_selfavatar_outside_blobdir() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
|
||||||
File::create(&avatar_src)
|
|
||||||
.unwrap()
|
|
||||||
.write_all(avatar_bytes)
|
|
||||||
.unwrap();
|
|
||||||
let avatar_blob = t.get_blobdir().join("avatar.jpg");
|
|
||||||
assert!(!avatar_blob.exists().await);
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(avatar_blob.exists().await);
|
|
||||||
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
|
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
|
|
||||||
let img = image::open(avatar_src).unwrap();
|
|
||||||
assert_eq!(img.width(), 1000);
|
|
||||||
assert_eq!(img.height(), 1000);
|
|
||||||
|
|
||||||
let img = image::open(avatar_blob).unwrap();
|
|
||||||
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
|
||||||
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_selfavatar_in_blobdir() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
|
||||||
File::create(&avatar_src)
|
|
||||||
.unwrap()
|
|
||||||
.write_all(avatar_bytes)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let img = image::open(&avatar_src).unwrap();
|
|
||||||
assert_eq!(img.width(), 900);
|
|
||||||
assert_eq!(img.height(), 900);
|
|
||||||
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
|
|
||||||
|
|
||||||
let img = image::open(avatar_src).unwrap();
|
|
||||||
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
|
|
||||||
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_selfavatar_copy_without_recode() {
|
|
||||||
let t = TestContext::new().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.get_blobdir().join("avatar.png");
|
|
||||||
assert!(!avatar_blob.exists().await);
|
|
||||||
t.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.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn test_media_quality_config_option() {
|
async fn test_media_quality_config_option() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ async fn get_autoconfig(
|
|||||||
) -> Option<Vec<ServerParams>> {
|
) -> Option<Vec<ServerParams>> {
|
||||||
if let Ok(res) = moz_autoconfigure(
|
if let Ok(res) = moz_autoconfigure(
|
||||||
ctx,
|
ctx,
|
||||||
format!(
|
&format!(
|
||||||
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
||||||
param_domain, param_addr_urlencoded
|
param_domain, param_addr_urlencoded
|
||||||
),
|
),
|
||||||
@@ -464,7 +464,7 @@ async fn get_autoconfig(
|
|||||||
if let Ok(res) = moz_autoconfigure(
|
if let Ok(res) = moz_autoconfigure(
|
||||||
ctx,
|
ctx,
|
||||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
||||||
format!(
|
&format!(
|
||||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||||
¶m_domain, ¶m_addr_urlencoded
|
¶m_domain, ¶m_addr_urlencoded
|
||||||
),
|
),
|
||||||
@@ -503,7 +503,7 @@ async fn get_autoconfig(
|
|||||||
// always SSL for Thunderbird's database
|
// always SSL for Thunderbird's database
|
||||||
if let Ok(res) = moz_autoconfigure(
|
if let Ok(res) = moz_autoconfigure(
|
||||||
ctx,
|
ctx,
|
||||||
format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
&format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
||||||
param,
|
param,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -251,10 +251,10 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
|
|||||||
|
|
||||||
pub(crate) async fn moz_autoconfigure(
|
pub(crate) async fn moz_autoconfigure(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
url: impl AsRef<str>,
|
url: &str,
|
||||||
param_in: &LoginParam,
|
param_in: &LoginParam,
|
||||||
) -> Result<Vec<ServerParams>, Error> {
|
) -> Result<Vec<ServerParams>, Error> {
|
||||||
let xml_raw = read_url(context, url.as_ref()).await?;
|
let xml_raw = read_url(context, url).await?;
|
||||||
|
|
||||||
let res = parse_serverparams(¶m_in.addr, &xml_raw);
|
let res = parse_serverparams(¶m_in.addr, &xml_raw);
|
||||||
if let Err(err) = &res {
|
if let Err(err) = &res {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! # Constants
|
//! # Constants
|
||||||
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -15,9 +16,10 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
|
|||||||
Eq,
|
Eq,
|
||||||
FromPrimitive,
|
FromPrimitive,
|
||||||
ToPrimitive,
|
ToPrimitive,
|
||||||
|
FromSql,
|
||||||
|
ToSql,
|
||||||
Serialize,
|
Serialize,
|
||||||
Deserialize,
|
Deserialize,
|
||||||
sqlx::Type,
|
|
||||||
)]
|
)]
|
||||||
#[repr(i8)]
|
#[repr(i8)]
|
||||||
pub enum Blocked {
|
pub enum Blocked {
|
||||||
@@ -32,7 +34,9 @@ impl Default for Blocked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum ShowEmails {
|
pub enum ShowEmails {
|
||||||
Off = 0,
|
Off = 0,
|
||||||
@@ -46,7 +50,9 @@ impl Default for ShowEmails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum MediaQuality {
|
pub enum MediaQuality {
|
||||||
Balanced = 0,
|
Balanced = 0,
|
||||||
@@ -59,7 +65,9 @@ impl Default for MediaQuality {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum KeyGenType {
|
pub enum KeyGenType {
|
||||||
Default = 0,
|
Default = 0,
|
||||||
@@ -73,7 +81,9 @@ impl Default for KeyGenType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
|
)]
|
||||||
#[repr(i8)]
|
#[repr(i8)]
|
||||||
pub enum VideochatType {
|
pub enum VideochatType {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -133,10 +143,11 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
|||||||
Eq,
|
Eq,
|
||||||
FromPrimitive,
|
FromPrimitive,
|
||||||
ToPrimitive,
|
ToPrimitive,
|
||||||
|
FromSql,
|
||||||
|
ToSql,
|
||||||
IntoStaticStr,
|
IntoStaticStr,
|
||||||
Serialize,
|
Serialize,
|
||||||
Deserialize,
|
Deserialize,
|
||||||
sqlx::Type,
|
|
||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Chattype {
|
pub enum Chattype {
|
||||||
@@ -247,9 +258,10 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
|||||||
Eq,
|
Eq,
|
||||||
FromPrimitive,
|
FromPrimitive,
|
||||||
ToPrimitive,
|
ToPrimitive,
|
||||||
|
FromSql,
|
||||||
|
ToSql,
|
||||||
Serialize,
|
Serialize,
|
||||||
Deserialize,
|
Deserialize,
|
||||||
sqlx::Type,
|
|
||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Viewtype {
|
pub enum Viewtype {
|
||||||
|
|||||||
557
src/contact.rs
557
src/contact.rs
@@ -1,13 +1,13 @@
|
|||||||
//! Contacts module
|
//! Contacts module
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
use anyhow::{bail, ensure, format_err, Result};
|
use anyhow::{bail, ensure, format_err, Result};
|
||||||
use async_std::path::PathBuf;
|
use async_std::path::PathBuf;
|
||||||
use async_std::prelude::*;
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::aheader::EncryptPreference;
|
use crate::aheader::EncryptPreference;
|
||||||
use crate::chat::ChatId;
|
use crate::chat::ChatId;
|
||||||
@@ -79,7 +79,7 @@ pub struct Contact {
|
|||||||
|
|
||||||
/// Possible origins of a contact.
|
/// Possible origins of a contact.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Origin {
|
pub enum Origin {
|
||||||
@@ -175,30 +175,36 @@ pub enum VerifiedStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Contact {
|
impl Contact {
|
||||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
|
||||||
let row = context
|
let mut contact = context
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(
|
.query_row(
|
||||||
sqlx::query(
|
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
|
||||||
FROM contacts c
|
FROM contacts c
|
||||||
WHERE c.id=?;",
|
WHERE c.id=?;",
|
||||||
)
|
paramsv![contact_id as i32],
|
||||||
.bind(contact_id),
|
|row| {
|
||||||
|
let name: String = row.get(0)?;
|
||||||
|
let addr: String = row.get(1)?;
|
||||||
|
let origin: Origin = row.get(2)?;
|
||||||
|
let blocked: Option<bool> = row.get(3)?;
|
||||||
|
let authname: String = row.get(4)?;
|
||||||
|
let param: String = row.get(5)?;
|
||||||
|
let status: Option<String> = row.get(6)?;
|
||||||
|
let contact = Self {
|
||||||
|
id: contact_id,
|
||||||
|
name,
|
||||||
|
authname,
|
||||||
|
addr,
|
||||||
|
blocked: blocked.unwrap_or_default(),
|
||||||
|
origin,
|
||||||
|
param: param.parse().unwrap_or_default(),
|
||||||
|
status: status.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
Ok(contact)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut contact = Contact {
|
|
||||||
id: contact_id,
|
|
||||||
name: row.try_get(0)?,
|
|
||||||
authname: row.try_get(4)?,
|
|
||||||
addr: row.try_get(1)?,
|
|
||||||
blocked: row.try_get::<Option<i32>, _>(3)?.unwrap_or_default() != 0,
|
|
||||||
origin: row.try_get(2)?,
|
|
||||||
param: row.try_get::<String, _>(5)?.parse().unwrap_or_default(),
|
|
||||||
status: row.try_get::<Option<String>, _>(6)?.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if contact_id == DC_CONTACT_ID_SELF {
|
if contact_id == DC_CONTACT_ID_SELF {
|
||||||
contact.name = stock_str::self_msg(context).await;
|
contact.name = stock_str::self_msg(context).await;
|
||||||
contact.addr = context
|
contact.addr = context
|
||||||
@@ -213,7 +219,6 @@ impl Contact {
|
|||||||
contact.name = stock_str::device_messages(context).await;
|
contact.name = stock_str::device_messages(context).await;
|
||||||
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
|
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(contact)
|
Ok(contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,21 +254,14 @@ impl Contact {
|
|||||||
/// a bunch of addresses.
|
/// a bunch of addresses.
|
||||||
///
|
///
|
||||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||||
pub async fn create(
|
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
|
||||||
context: &Context,
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
addr: impl AsRef<str>,
|
|
||||||
) -> Result<u32> {
|
|
||||||
let name = improve_single_line_input(name);
|
let name = improve_single_line_input(name);
|
||||||
ensure!(
|
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
|
||||||
!addr.as_ref().is_empty(),
|
|
||||||
"Cannot create contact with empty address"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
let (name, addr) = sanitize_name_and_addr(&name, addr);
|
||||||
|
|
||||||
let (contact_id, sth_modified) =
|
let (contact_id, sth_modified) =
|
||||||
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?;
|
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
|
||||||
let blocked = Contact::is_blocked_load(context, contact_id).await;
|
let blocked = Contact::is_blocked_load(context, contact_id).await;
|
||||||
match sth_modified {
|
match sth_modified {
|
||||||
Modifier::None => {}
|
Modifier::None => {}
|
||||||
@@ -285,10 +283,8 @@ impl Contact {
|
|||||||
if context
|
if context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;")
|
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||||
.bind(MessageState::InNoticed)
|
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||||
.bind(id as i32)
|
|
||||||
.bind(MessageState::InFresh),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -322,18 +318,16 @@ impl Contact {
|
|||||||
let id = context
|
let id = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query(
|
"SELECT id FROM contacts \
|
||||||
"SELECT id FROM contacts \
|
WHERE addr=?1 COLLATE NOCASE \
|
||||||
WHERE addr=?1 COLLATE NOCASE \
|
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
paramsv![
|
||||||
)
|
addr_normalized,
|
||||||
.bind(addr_normalized)
|
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
min_origin as u32,
|
||||||
.bind(min_origin),
|
],
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,19 +358,16 @@ impl Contact {
|
|||||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
|
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
|
||||||
pub(crate) async fn add_or_lookup(
|
pub(crate) async fn add_or_lookup(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
name: impl AsRef<str>,
|
name: &str,
|
||||||
addr: impl AsRef<str>,
|
addr: &str,
|
||||||
mut origin: Origin,
|
mut origin: Origin,
|
||||||
) -> Result<(u32, Modifier)> {
|
) -> Result<(u32, Modifier)> {
|
||||||
let mut sth_modified = Modifier::None;
|
let mut sth_modified = Modifier::None;
|
||||||
|
|
||||||
ensure!(
|
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||||
!addr.as_ref().is_empty(),
|
|
||||||
"Can not add_or_lookup empty address"
|
|
||||||
);
|
|
||||||
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
||||||
|
|
||||||
let addr = addr_normalize(addr.as_ref()).to_string();
|
let addr = addr_normalize(addr).to_string();
|
||||||
let addr_self = context
|
let addr_self = context
|
||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
.await?
|
.await?
|
||||||
@@ -391,16 +382,12 @@ impl Contact {
|
|||||||
context,
|
context,
|
||||||
"Bad address \"{}\" for contact \"{}\".",
|
"Bad address \"{}\" for contact \"{}\".",
|
||||||
addr,
|
addr,
|
||||||
if !name.as_ref().is_empty() {
|
if !name.is_empty() { name } else { "<unset>" },
|
||||||
name.as_ref()
|
|
||||||
} else {
|
|
||||||
"<unset>"
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
bail!("Bad address supplied: {:?}", addr);
|
bail!("Bad address supplied: {:?}", addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut name = name.as_ref();
|
let mut name = name;
|
||||||
#[allow(clippy::collapsible_if)]
|
#[allow(clippy::collapsible_if)]
|
||||||
if origin <= Origin::OutgoingTo {
|
if origin <= Origin::OutgoingTo {
|
||||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||||
@@ -433,23 +420,21 @@ impl Contact {
|
|||||||
|
|
||||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
|
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(
|
.query_row(
|
||||||
sqlx::query(
|
"SELECT id, name, addr, origin, authname \
|
||||||
"SELECT id, name, addr, origin, authname \
|
FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||||
FROM contacts WHERE addr=? COLLATE NOCASE;",
|
paramsv![addr.to_string()],
|
||||||
)
|
|row| {
|
||||||
.bind(addr.to_string()),
|
let row_id: isize = row.get(0)?;
|
||||||
|
let row_name: String = row.get(1)?;
|
||||||
|
let row_addr: String = row.get(2)?;
|
||||||
|
let row_origin: Origin = row.get(3)?;
|
||||||
|
let row_authname: String = row.get(4)?;
|
||||||
|
|
||||||
|
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.and_then(|row| {
|
|
||||||
let row_id = row.try_get(0)?;
|
|
||||||
let row_name: String = row.try_get(1)?;
|
|
||||||
let row_addr: String = row.try_get(2)?;
|
|
||||||
let row_origin: Origin = row.try_get(3)?;
|
|
||||||
let row_authname: String = row.try_get(4)?;
|
|
||||||
|
|
||||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
let update_name = manual && name != row_name;
|
let update_name = manual && name != row_name;
|
||||||
let update_authname = !manual
|
let update_authname = !manual
|
||||||
@@ -458,7 +443,8 @@ impl Contact {
|
|||||||
&& (origin >= row_origin
|
&& (origin >= row_origin
|
||||||
|| origin == Origin::IncomingUnknownFrom
|
|| origin == Origin::IncomingUnknownFrom
|
||||||
|| row_authname.is_empty());
|
|| row_authname.is_empty());
|
||||||
row_id = id;
|
|
||||||
|
row_id = u32::try_from(id)?;
|
||||||
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
||||||
update_addr = true;
|
update_addr = true;
|
||||||
}
|
}
|
||||||
@@ -469,36 +455,39 @@ impl Contact {
|
|||||||
row_name
|
row_name
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = sqlx::query(
|
context
|
||||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
.sql
|
||||||
)
|
.execute(
|
||||||
.bind(&new_name)
|
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||||
.bind(if update_addr {
|
paramsv![
|
||||||
addr.to_string()
|
new_name,
|
||||||
} else {
|
if update_addr {
|
||||||
row_addr
|
addr.to_string()
|
||||||
})
|
} else {
|
||||||
.bind(if origin > row_origin {
|
row_addr
|
||||||
origin
|
},
|
||||||
} else {
|
if origin > row_origin {
|
||||||
row_origin
|
origin
|
||||||
})
|
} else {
|
||||||
.bind(if update_authname {
|
row_origin
|
||||||
name.to_string()
|
},
|
||||||
} else {
|
if update_authname {
|
||||||
row_authname
|
name.to_string()
|
||||||
})
|
} else {
|
||||||
.bind(row_id);
|
row_authname
|
||||||
|
},
|
||||||
context.sql.execute(query).await.ok();
|
row_id
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
if update_name {
|
if update_name {
|
||||||
// Update the contact name also if it is used as a group name.
|
// Update the contact name also if it is used as a group name.
|
||||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||||
let chat_id = context.sql.query_get_value::<_, u32>(
|
let chat_id: Option<i32> = context.sql.query_get_value(
|
||||||
sqlx::query(
|
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)"
|
paramsv![Chattype::Single, isize::try_from(row_id)?]
|
||||||
).bind(Chattype::Single).bind(row_id)
|
|
||||||
).await?;
|
).await?;
|
||||||
if let Some(chat_id) = chat_id {
|
if let Some(chat_id) = chat_id {
|
||||||
let contact = Contact::get_by_id(context, row_id as u32).await?;
|
let contact = Contact::get_by_id(context, row_id as u32).await?;
|
||||||
@@ -506,10 +495,8 @@ impl Contact {
|
|||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3")
|
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3",
|
||||||
.bind(&chat_name)
|
paramsv![chat_name, chat_id, chat_name],
|
||||||
.bind(chat_id)
|
|
||||||
.bind(&chat_name),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -517,8 +504,9 @@ impl Contact {
|
|||||||
Ok(count) => {
|
Ok(count) => {
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
// Chat name updated
|
// Chat name updated
|
||||||
context
|
context.emit_event(EventType::ChatModified(ChatId::new(
|
||||||
.emit_event(EventType::ChatModified(ChatId::new(chat_id)));
|
chat_id.try_into()?,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,33 +521,33 @@ impl Contact {
|
|||||||
if let Ok(new_row_id) = context
|
if let Ok(new_row_id) = context
|
||||||
.sql
|
.sql
|
||||||
.insert(
|
.insert(
|
||||||
sqlx::query(
|
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
||||||
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
paramsv![
|
||||||
)
|
if update_name {
|
||||||
.bind(if update_name {
|
name.to_string()
|
||||||
name.to_string()
|
} else {
|
||||||
} else {
|
"".to_string()
|
||||||
"".to_string()
|
},
|
||||||
})
|
addr,
|
||||||
.bind(&addr)
|
origin,
|
||||||
.bind(origin)
|
if update_authname {
|
||||||
.bind(if update_authname {
|
name.to_string()
|
||||||
name.to_string()
|
} else {
|
||||||
} else {
|
"".to_string()
|
||||||
"".to_string()
|
}
|
||||||
}),
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
row_id = new_row_id;
|
row_id = u32::try_from(new_row_id)?;
|
||||||
sth_modified = Modifier::Created;
|
sth_modified = Modifier::Created;
|
||||||
info!(context, "added contact id={} addr={}", row_id, &addr);
|
info!(context, "added contact id={} addr={} origin={:?}", row_id, &addr, &origin);
|
||||||
} else {
|
} else {
|
||||||
error!(context, "Cannot add contact.");
|
error!(context, "Cannot add contact.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((u32::try_from(row_id)?, sth_modified))
|
Ok((row_id, sth_modified))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a number of contacts.
|
/// Add a number of contacts.
|
||||||
@@ -579,13 +567,13 @@ impl Contact {
|
|||||||
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
|
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
|
||||||
///
|
///
|
||||||
/// Returns the number of modified contacts.
|
/// Returns the number of modified contacts.
|
||||||
pub async fn add_address_book(context: &Context, addr_book: impl AsRef<str>) -> Result<usize> {
|
pub async fn add_address_book(context: &Context, addr_book: &str) -> Result<usize> {
|
||||||
let mut modify_cnt = 0;
|
let mut modify_cnt = 0;
|
||||||
|
|
||||||
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
|
for (name, addr) in split_address_book(addr_book).into_iter() {
|
||||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||||
let name = normalize_name(name);
|
let name = normalize_name(name);
|
||||||
match Contact::add_or_lookup(context, name, &addr, Origin::AddressBook).await {
|
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
@@ -631,19 +619,11 @@ impl Contact {
|
|||||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||||
|
|
||||||
if flag_verified_only || query.is_some() {
|
if flag_verified_only || query.is_some() {
|
||||||
let s3str_like_cmd = format!(
|
let s3str_like_cmd = format!("%{}%", query.as_ref().map(|s| s.as_ref()).unwrap_or(""));
|
||||||
"%{}%",
|
context
|
||||||
query
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.as_ref().to_string())
|
|
||||||
.unwrap_or_default()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut rows = context
|
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT c.id FROM contacts c \
|
||||||
"SELECT c.id FROM contacts c \
|
|
||||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||||
WHERE c.addr!=?1 \
|
WHERE c.addr!=?1 \
|
||||||
AND c.id>?2 \
|
AND c.id>?2 \
|
||||||
@@ -652,19 +632,23 @@ impl Contact {
|
|||||||
AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \
|
AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \
|
||||||
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||||
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||||
)
|
paramsv![
|
||||||
.bind(&self_addr)
|
self_addr,
|
||||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||||
.bind(Origin::IncomingReplyTo)
|
Origin::IncomingReplyTo,
|
||||||
.bind(&s3str_like_cmd)
|
s3str_like_cmd,
|
||||||
.bind(&s3str_like_cmd)
|
s3str_like_cmd,
|
||||||
.bind(if flag_verified_only { 0i32 } else { 1i32 }),
|
if flag_verified_only { 0i32 } else { 1i32 },
|
||||||
|
],
|
||||||
|
|row| row.get::<_, i32>(0),
|
||||||
|
|ids| {
|
||||||
|
for id in ids {
|
||||||
|
ret.push(id? as u32);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.map(|row| row?.try_get(0));
|
|
||||||
while let Some(id) = rows.next().await {
|
|
||||||
ret.push(id?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let self_name = context
|
let self_name = context
|
||||||
.get_config(Config::Displayname)
|
.get_config(Config::Displayname)
|
||||||
@@ -685,27 +669,29 @@ impl Contact {
|
|||||||
} else {
|
} else {
|
||||||
add_self = true;
|
add_self = true;
|
||||||
|
|
||||||
let mut rows = context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT id FROM contacts
|
||||||
"SELECT id FROM contacts
|
|
||||||
WHERE addr!=?1
|
WHERE addr!=?1
|
||||||
AND id>?2
|
AND id>?2
|
||||||
AND origin>=?3
|
AND origin>=?3
|
||||||
AND blocked=0
|
AND blocked=0
|
||||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||||
)
|
paramsv![
|
||||||
.bind(self_addr)
|
self_addr,
|
||||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||||
.bind(Origin::IncomingReplyTo),
|
Origin::IncomingReplyTo
|
||||||
|
],
|
||||||
|
|row| row.get::<_, i32>(0),
|
||||||
|
|ids| {
|
||||||
|
for id in ids {
|
||||||
|
ret.push(id? as u32);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.map(|row| row?.try_get(0));
|
|
||||||
|
|
||||||
while let Some(id) = rows.next().await {
|
|
||||||
ret.push(id?);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flag_add_self && add_self {
|
if flag_add_self && add_self {
|
||||||
@@ -721,38 +707,38 @@ impl Contact {
|
|||||||
// from the users perspective,
|
// from the users perspective,
|
||||||
// there is not much difference in an email- and a mailinglist-address)
|
// there is not much difference in an email- and a mailinglist-address)
|
||||||
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
|
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
|
||||||
let mut rows = context
|
let blocked_mailinglists = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;")
|
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
|
||||||
.bind(Chattype::Mailinglist)
|
paramsv![Chattype::Mailinglist, Blocked::Manually],
|
||||||
.bind(Blocked::Manually),
|
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||||
|
|rows| {
|
||||||
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
for (name, grpid) in blocked_mailinglists {
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
let name = row.try_get::<String, _>(0)?;
|
|
||||||
let grpid = row.try_get::<String, _>(1)?;
|
|
||||||
|
|
||||||
if !context
|
if !context
|
||||||
.sql
|
.sql
|
||||||
.exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid))
|
.exists(
|
||||||
|
"SELECT COUNT(id) FROM contacts WHERE addr=?;",
|
||||||
|
paramsv![grpid],
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid))
|
.execute("INSERT INTO contacts (addr) VALUES (?);", paramsv![grpid])
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
// always do an update in case the blocking is reset or name is changed
|
// always do an update in case the blocking is reset or name is changed
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;")
|
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;",
|
||||||
.bind(name)
|
paramsv![name, Origin::MailinglistAddress, grpid],
|
||||||
.bind(Origin::MailinglistAddress)
|
|
||||||
.bind(&grpid),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -763,8 +749,8 @@ impl Contact {
|
|||||||
let count = context
|
let count = context
|
||||||
.sql
|
.sql
|
||||||
.count(
|
.count(
|
||||||
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0")
|
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||||
.bind(DC_CONTACT_ID_LAST_SPECIAL),
|
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(count as usize)
|
Ok(count as usize)
|
||||||
@@ -781,16 +767,16 @@ impl Contact {
|
|||||||
|
|
||||||
let list = context
|
let list = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
|
||||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||||
).bind(DC_CONTACT_ID_LAST_SPECIAL)
|
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||||
|
|row| row.get::<_, u32>(0),
|
||||||
|
|ids| {
|
||||||
|
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
|
||||||
.map(|row| row?.try_get::<u32, _>(0))
|
|
||||||
.collect::<sqlx::Result<Vec<_>>>()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,14 +829,14 @@ impl Contact {
|
|||||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||||
cat_fingerprint(
|
cat_fingerprint(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
peerstate.addr.clone(),
|
&peerstate.addr,
|
||||||
&fingerprint_other_verified,
|
&fingerprint_other_verified,
|
||||||
&fingerprint_other_unverified,
|
&fingerprint_other_unverified,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cat_fingerprint(
|
cat_fingerprint(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
peerstate.addr.clone(),
|
&peerstate.addr,
|
||||||
&fingerprint_other_verified,
|
&fingerprint_other_verified,
|
||||||
&fingerprint_other_unverified,
|
&fingerprint_other_unverified,
|
||||||
);
|
);
|
||||||
@@ -877,8 +863,8 @@ impl Contact {
|
|||||||
let count_contacts = context
|
let count_contacts = context
|
||||||
.sql
|
.sql
|
||||||
.count(
|
.count(
|
||||||
sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;")
|
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||||
.bind(contact_id),
|
paramsv![contact_id as i32],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -886,9 +872,8 @@ impl Contact {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.count(
|
.count(
|
||||||
sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;")
|
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||||
.bind(contact_id)
|
paramsv![contact_id as i32, contact_id as i32],
|
||||||
.bind(contact_id),
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
@@ -898,7 +883,10 @@ impl Contact {
|
|||||||
if count_msgs == 0 {
|
if count_msgs == 0 {
|
||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32))
|
.execute(
|
||||||
|
"DELETE FROM contacts WHERE id=?;",
|
||||||
|
paramsv![contact_id as i32],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -907,7 +895,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(context, "delete_contact {} failed ({})", contact_id, err);
|
error!(context, "delete_contact {} failed ({})", contact_id, err);
|
||||||
return Err(err.into());
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,9 +923,8 @@ impl Contact {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE contacts SET param=? WHERE id=?")
|
"UPDATE contacts SET param=? WHERE id=?",
|
||||||
.bind(self.param.to_string())
|
paramsv![self.param.to_string(), self.id as i32],
|
||||||
.bind(self.id as i32),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -948,9 +935,8 @@ impl Contact {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE contacts SET status=? WHERE id=?")
|
"UPDATE contacts SET status=? WHERE id=?",
|
||||||
.bind(&self.status)
|
paramsv![self.status, self.id as i32],
|
||||||
.bind(self.id as i32),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1092,18 +1078,14 @@ impl Contact {
|
|||||||
VerifiedStatus::Unverified
|
VerifiedStatus::Unverified
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn addr_equals_contact(
|
pub async fn addr_equals_contact(context: &Context, addr: &str, contact_id: u32) -> bool {
|
||||||
context: &Context,
|
if addr.is_empty() {
|
||||||
addr: impl AsRef<str>,
|
|
||||||
contact_id: u32,
|
|
||||||
) -> bool {
|
|
||||||
if addr.as_ref().is_empty() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||||
if !contact.addr.is_empty() {
|
if !contact.addr.is_empty() {
|
||||||
let normalized_addr = addr_normalize(addr.as_ref());
|
let normalized_addr = addr_normalize(addr);
|
||||||
if contact.addr == normalized_addr {
|
if contact.addr == normalized_addr {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1121,8 +1103,8 @@ impl Contact {
|
|||||||
let count = context
|
let count = context
|
||||||
.sql
|
.sql
|
||||||
.count(
|
.count(
|
||||||
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;")
|
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||||
.bind(DC_CONTACT_ID_LAST_SPECIAL),
|
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(count)
|
Ok(count)
|
||||||
@@ -1135,7 +1117,10 @@ impl Contact {
|
|||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id))
|
.exists(
|
||||||
|
"SELECT COUNT(*) FROM contacts WHERE id=?;",
|
||||||
|
paramsv![contact_id as i32],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -1144,10 +1129,8 @@ impl Contact {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE contacts SET origin=? WHERE id=? AND origin<?;")
|
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||||
.bind(origin)
|
paramsv![origin, contact_id as i32, origin],
|
||||||
.bind(contact_id)
|
|
||||||
.bind(origin),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -1171,23 +1154,23 @@ pub fn addr_normalize(addr: &str) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
|
fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||||
(
|
(
|
||||||
if name.as_ref().is_empty() {
|
if name.is_empty() {
|
||||||
captures
|
captures
|
||||||
.get(1)
|
.get(1)
|
||||||
.map_or("".to_string(), |m| normalize_name(m.as_str()))
|
.map_or("".to_string(), |m| normalize_name(m.as_str()))
|
||||||
} else {
|
} else {
|
||||||
name.as_ref().to_string()
|
name.to_string()
|
||||||
},
|
},
|
||||||
captures
|
captures
|
||||||
.get(2)
|
.get(2)
|
||||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(name.as_ref().to_string(), addr.as_ref().to_string())
|
(name.to_string(), addr.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1201,9 +1184,8 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
|||||||
&& context
|
&& context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE contacts SET blocked=? WHERE id=?;")
|
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||||
.bind(new_blocking as i32)
|
paramsv![new_blocking as i32, contact_id as i32],
|
||||||
.bind(contact_id),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -1216,18 +1198,14 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
|||||||
if context
|
if context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
r#"
|
||||||
r#"
|
|
||||||
UPDATE chats
|
UPDATE chats
|
||||||
SET blocked=?
|
SET blocked=?
|
||||||
WHERE type=? AND id IN (
|
WHERE type=? AND id IN (
|
||||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||||
);
|
);
|
||||||
"#,
|
"#,
|
||||||
)
|
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||||
.bind(new_blocking)
|
|
||||||
.bind(Chattype::Single)
|
|
||||||
.bind(contact_id),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -1298,13 +1276,31 @@ pub(crate) async fn set_profile_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets contact status.
|
/// Sets contact status.
|
||||||
pub(crate) async fn set_status(context: &Context, contact_id: u32, status: String) -> Result<()> {
|
///
|
||||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus. This
|
||||||
|
/// is only done if message is sent from Delta Chat and it is encrypted, to synchronize signature
|
||||||
|
/// between Delta Chat devices.
|
||||||
|
pub(crate) async fn set_status(
|
||||||
|
context: &Context,
|
||||||
|
contact_id: u32,
|
||||||
|
status: String,
|
||||||
|
encrypted: bool,
|
||||||
|
has_chat_version: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
if contact_id == DC_CONTACT_ID_SELF {
|
||||||
|
if encrypted && has_chat_version {
|
||||||
|
context
|
||||||
|
.set_config(Config::Selfstatus, Some(&status))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||||
|
|
||||||
if contact.status != status {
|
if contact.status != status {
|
||||||
contact.status = status;
|
contact.status = status;
|
||||||
contact.update_status(context).await?;
|
contact.update_status(context).await?;
|
||||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1331,13 +1327,13 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
|||||||
|
|
||||||
fn cat_fingerprint(
|
fn cat_fingerprint(
|
||||||
ret: &mut String,
|
ret: &mut String,
|
||||||
addr: impl AsRef<str>,
|
addr: &str,
|
||||||
fingerprint_verified: impl AsRef<str>,
|
fingerprint_verified: impl AsRef<str>,
|
||||||
fingerprint_unverified: impl AsRef<str>,
|
fingerprint_unverified: impl AsRef<str>,
|
||||||
) {
|
) {
|
||||||
*ret += &format!(
|
*ret += &format!(
|
||||||
"\n\n{}:\n{}",
|
"\n\n{}:\n{}",
|
||||||
addr.as_ref(),
|
addr,
|
||||||
if !fingerprint_verified.as_ref().is_empty() {
|
if !fingerprint_verified.as_ref().is_empty() {
|
||||||
fingerprint_verified.as_ref()
|
fingerprint_verified.as_ref()
|
||||||
} else {
|
} else {
|
||||||
@@ -1350,7 +1346,7 @@ fn cat_fingerprint(
|
|||||||
{
|
{
|
||||||
*ret += &format!(
|
*ret += &format!(
|
||||||
"\n\n{} (alternative):\n{}",
|
"\n\n{} (alternative):\n{}",
|
||||||
addr.as_ref(),
|
addr,
|
||||||
fingerprint_unverified.as_ref()
|
fingerprint_unverified.as_ref()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1391,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::chat::send_text_msg;
|
use crate::chat::send_text_msg;
|
||||||
|
use crate::message::Message;
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1900,4 +1897,70 @@ CCCB 5AA9 F6E1 141C 9431
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
|
||||||
|
/// synchronized when the message is not encrypted.
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_synchronize_status() -> Result<()> {
|
||||||
|
// Alice has two devices.
|
||||||
|
let alice1 = TestContext::new_alice().await;
|
||||||
|
let alice2 = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
// Bob has one device.
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let default_status = alice1.get_config(Config::Selfstatus).await?;
|
||||||
|
|
||||||
|
alice1
|
||||||
|
.set_config(Config::Selfstatus, Some("New status"))
|
||||||
|
.await?;
|
||||||
|
let chat = alice1
|
||||||
|
.create_chat_with_contact("Bob", "bob@example.net")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Alice sends a message to Bob from the first device.
|
||||||
|
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
|
||||||
|
let sent_msg = alice1.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Message is not encrypted.
|
||||||
|
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||||
|
assert!(!message.get_showpadlock());
|
||||||
|
|
||||||
|
// Alice's second devices receives a copy of outgoing message.
|
||||||
|
alice2.recv_msg(&sent_msg).await;
|
||||||
|
|
||||||
|
// Bob receives message.
|
||||||
|
bob.recv_msg(&sent_msg).await;
|
||||||
|
|
||||||
|
// Message was not encrypted, so status is not copied.
|
||||||
|
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
|
||||||
|
|
||||||
|
// Bob replies.
|
||||||
|
let chat = bob
|
||||||
|
.create_chat_with_contact("Alice", "alice@example.com")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||||
|
let sent_msg = bob.pop_sent_msg().await;
|
||||||
|
alice1.recv_msg(&sent_msg).await;
|
||||||
|
alice2.recv_msg(&sent_msg).await;
|
||||||
|
|
||||||
|
// Alice sends second message.
|
||||||
|
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
|
||||||
|
let sent_msg = alice1.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Second message is encrypted.
|
||||||
|
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
|
||||||
|
assert!(message.get_showpadlock());
|
||||||
|
|
||||||
|
// Alice's second devices receives a copy of second outgoing message.
|
||||||
|
alice2.recv_msg(&sent_msg).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
alice2.get_config(Config::Selfstatus).await?,
|
||||||
|
Some("New status".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/context.rs
128
src/context.rs
@@ -6,14 +6,12 @@ use std::ops::Deref;
|
|||||||
use std::time::{Instant, SystemTime};
|
use std::time::{Instant, SystemTime};
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use async_std::prelude::*;
|
|
||||||
use async_std::{
|
use async_std::{
|
||||||
channel::{self, Receiver, Sender},
|
channel::{self, Receiver, Sender},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex, RwLock},
|
sync::{Arc, Mutex, RwLock},
|
||||||
task,
|
task,
|
||||||
};
|
};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::chat::{get_chat_cnt, ChatId};
|
use crate::chat::{get_chat_cnt, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -91,7 +89,7 @@ pub struct RunningState {
|
|||||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||||
let mut res = BTreeMap::new();
|
let mut res = BTreeMap::new();
|
||||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||||
res.insert("sqlite_version", crate::sql::version().to_string());
|
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||||
res.insert("num_cpus", num_cpus::get().to_string());
|
res.insert("num_cpus", num_cpus::get().to_string());
|
||||||
res.insert("level", "awesome".into());
|
res.insert("level", "awesome".into());
|
||||||
@@ -290,7 +288,7 @@ impl Context {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let journal_mode = self
|
let journal_mode = self
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(sqlx::query("PRAGMA journal_mode;"))
|
.query_get_value("PRAGMA journal_mode;", paramsv![])
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||||
@@ -299,12 +297,12 @@ impl Context {
|
|||||||
|
|
||||||
let prv_key_cnt = self
|
let prv_key_cnt = self
|
||||||
.sql
|
.sql
|
||||||
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
|
.count("SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let pub_key_cnt = self
|
let pub_key_cnt = self
|
||||||
.sql
|
.sql
|
||||||
.count(sqlx::query("SELECT COUNT(*) FROM acpeerstates;"))
|
.count("SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||||
Ok(key) => key.fingerprint().hex(),
|
Ok(key) => key.fingerprint().hex(),
|
||||||
@@ -431,8 +429,8 @@ impl Context {
|
|||||||
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
||||||
let list = self
|
let list = self
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(concat!(
|
concat!(
|
||||||
"SELECT m.id",
|
"SELECT m.id",
|
||||||
" FROM msgs m",
|
" FROM msgs m",
|
||||||
" LEFT JOIN contacts ct",
|
" LEFT JOIN contacts ct",
|
||||||
@@ -446,13 +444,17 @@ impl Context {
|
|||||||
" AND c.blocked=0",
|
" AND c.blocked=0",
|
||||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||||
))
|
),
|
||||||
.bind(MessageState::InFresh)
|
paramsv![MessageState::InFresh, time()],
|
||||||
.bind(time()),
|
|row| row.get::<_, MsgId>(0),
|
||||||
|
|rows| {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
list.push(row?);
|
||||||
|
}
|
||||||
|
Ok(list)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
|
||||||
.map(|row| row?.try_get("id"))
|
|
||||||
.collect::<sqlx::Result<_>>()
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
@@ -461,22 +463,31 @@ impl Context {
|
|||||||
///
|
///
|
||||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||||
/// is `None` this searches messages from all chats.
|
/// is `None` this searches messages from all chats.
|
||||||
pub async fn search_msgs(
|
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
|
||||||
&self,
|
let real_query = query.trim();
|
||||||
chat_id: Option<ChatId>,
|
|
||||||
query: impl AsRef<str>,
|
|
||||||
) -> Result<Vec<MsgId>> {
|
|
||||||
let real_query = query.as_ref().trim();
|
|
||||||
if real_query.is_empty() {
|
if real_query.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let str_like_in_text = format!("%{}%", real_query);
|
let str_like_in_text = format!("%{}%", real_query);
|
||||||
|
|
||||||
|
let do_query = |query, params| {
|
||||||
|
self.sql.query_map(
|
||||||
|
query,
|
||||||
|
params,
|
||||||
|
|row| row.get::<_, MsgId>("id"),
|
||||||
|
|rows| {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
for id in rows {
|
||||||
|
ret.push(id?);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let list = if let Some(chat_id) = chat_id {
|
let list = if let Some(chat_id) = chat_id {
|
||||||
self.sql
|
do_query(
|
||||||
.fetch(
|
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||||
sqlx::query(
|
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
|
||||||
FROM msgs m
|
FROM msgs m
|
||||||
LEFT JOIN contacts ct
|
LEFT JOIN contacts ct
|
||||||
ON m.from_id=ct.id
|
ON m.from_id=ct.id
|
||||||
@@ -485,18 +496,9 @@ impl Context {
|
|||||||
AND ct.blocked=0
|
AND ct.blocked=0
|
||||||
AND txt LIKE ?
|
AND txt LIKE ?
|
||||||
ORDER BY m.timestamp,m.id;",
|
ORDER BY m.timestamp,m.id;",
|
||||||
)
|
paramsv![chat_id, str_like_in_text],
|
||||||
.bind(chat_id)
|
)
|
||||||
.bind(str_like_in_text),
|
.await?
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.map(|row| {
|
|
||||||
let row = row?;
|
|
||||||
let id = row.try_get::<MsgId, _>("id")?;
|
|
||||||
Ok(id)
|
|
||||||
})
|
|
||||||
.collect::<sqlx::Result<Vec<MsgId>>>()
|
|
||||||
.await?
|
|
||||||
} else {
|
} else {
|
||||||
// For performance reasons results are sorted only by `id`, that is in the order of
|
// For performance reasons results are sorted only by `id`, that is in the order of
|
||||||
// message reception.
|
// message reception.
|
||||||
@@ -508,10 +510,8 @@ impl Context {
|
|||||||
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
|
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
|
||||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||||
self.sql
|
do_query(
|
||||||
.fetch(
|
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||||
sqlx::query(
|
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
|
||||||
FROM msgs m
|
FROM msgs m
|
||||||
LEFT JOIN contacts ct
|
LEFT JOIN contacts ct
|
||||||
ON m.from_id=ct.id
|
ON m.from_id=ct.id
|
||||||
@@ -523,44 +523,35 @@ impl Context {
|
|||||||
AND ct.blocked=0
|
AND ct.blocked=0
|
||||||
AND m.txt LIKE ?
|
AND m.txt LIKE ?
|
||||||
ORDER BY m.id DESC LIMIT 1000",
|
ORDER BY m.id DESC LIMIT 1000",
|
||||||
)
|
paramsv![str_like_in_text],
|
||||||
.bind(str_like_in_text),
|
)
|
||||||
)
|
.await?
|
||||||
.await?
|
|
||||||
.map(|row| {
|
|
||||||
let row = row?;
|
|
||||||
let id = row.try_get::<MsgId, _>("id")?;
|
|
||||||
Ok(id)
|
|
||||||
})
|
|
||||||
.collect::<sqlx::Result<Vec<MsgId>>>()
|
|
||||||
.await?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> {
|
||||||
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
|
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
|
||||||
Ok(inbox == Some(folder_name.as_ref().to_string()))
|
Ok(inbox.as_deref() == Some(folder_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
|
||||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||||
|
|
||||||
Ok(sentbox == Some(folder_name.as_ref().to_string()))
|
Ok(sentbox.as_deref() == Some(folder_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||||
|
|
||||||
Ok(mvbox == Some(folder_name.as_ref().to_string()))
|
Ok(mvbox.as_deref() == Some(folder_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
|
||||||
let is_spam = self.get_config(Config::ConfiguredSpamFolder).await?
|
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
|
||||||
== Some(folder_name.as_ref().to_string());
|
|
||||||
|
|
||||||
Ok(is_spam)
|
Ok(spam.as_deref() == Some(folder_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
||||||
@@ -613,8 +604,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::chat::{
|
use crate::chat::{
|
||||||
create_by_contact_id, get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat,
|
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||||
MuteDuration,
|
|
||||||
};
|
};
|
||||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||||
use crate::dc_receive_imf::dc_receive_imf;
|
use crate::dc_receive_imf::dc_receive_imf;
|
||||||
@@ -747,9 +737,8 @@ mod tests {
|
|||||||
// we need to modify the database directly
|
// we need to modify the database directly
|
||||||
t.sql
|
t.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;")
|
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||||
.bind(time() - 3600)
|
paramsv![time() - 3600, bob.id],
|
||||||
.bind(bob.id),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -766,7 +755,10 @@ mod tests {
|
|||||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||||
// that results in "muted forever" by definition.
|
// that results in "muted forever" by definition.
|
||||||
t.sql
|
t.sql
|
||||||
.execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id))
|
.execute(
|
||||||
|
"UPDATE chats SET muted_until=-2 WHERE id=?;",
|
||||||
|
paramsv![bob.id],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||||
@@ -895,7 +887,7 @@ mod tests {
|
|||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn test_search_msgs() -> Result<()> {
|
async fn test_search_msgs() -> Result<()> {
|
||||||
let alice = TestContext::new_alice().await;
|
let alice = TestContext::new_alice().await;
|
||||||
let self_talk = create_by_contact_id(&alice, DC_CONTACT_ID_SELF).await?;
|
let self_talk = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
|
||||||
let chat = alice
|
let chat = alice
|
||||||
.create_chat_with_contact("Bob", "bob@example.org")
|
.create_chat_with_contact("Bob", "bob@example.org")
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, format_err, Result};
|
use anyhow::{bail, ensure, format_err, Result};
|
||||||
use async_std::prelude::*;
|
|
||||||
use itertools::join;
|
use itertools::join;
|
||||||
use mailparse::SingleInfo;
|
use mailparse::SingleInfo;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
Blocked, Chattype, ShowEmails, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_LAST_SPECIAL,
|
Blocked, Chattype, ShowEmails, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_LAST_SPECIAL,
|
||||||
@@ -53,7 +51,7 @@ enum CreateEvent {
|
|||||||
pub async fn dc_receive_imf(
|
pub async fn dc_receive_imf(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
server_folder: impl AsRef<str>,
|
server_folder: &str,
|
||||||
server_uid: u32,
|
server_uid: u32,
|
||||||
seen: bool,
|
seen: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -63,17 +61,14 @@ pub async fn dc_receive_imf(
|
|||||||
pub(crate) async fn dc_receive_imf_inner(
|
pub(crate) async fn dc_receive_imf_inner(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
server_folder: impl AsRef<str>,
|
server_folder: &str,
|
||||||
server_uid: u32,
|
server_uid: u32,
|
||||||
seen: bool,
|
seen: bool,
|
||||||
fetching_existing_messages: bool,
|
fetching_existing_messages: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Receiving message {}/{}, seen={}...",
|
"Receiving message {}/{}, seen={}...", server_folder, server_uid, seen
|
||||||
server_folder.as_ref(),
|
|
||||||
server_uid,
|
|
||||||
seen
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
|
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
|
||||||
@@ -178,7 +173,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
|||||||
imf_raw,
|
imf_raw,
|
||||||
incoming,
|
incoming,
|
||||||
incoming_origin,
|
incoming_origin,
|
||||||
server_folder.as_ref(),
|
server_folder,
|
||||||
server_uid,
|
server_uid,
|
||||||
&to_ids,
|
&to_ids,
|
||||||
&rfc724_mid,
|
&rfc724_mid,
|
||||||
@@ -245,6 +240,8 @@ pub(crate) async fn dc_receive_imf_inner(
|
|||||||
context,
|
context,
|
||||||
from_id,
|
from_id,
|
||||||
mime_parser.footer.clone().unwrap_or_default(),
|
mime_parser.footer.clone().unwrap_or_default(),
|
||||||
|
mime_parser.was_encrypted(),
|
||||||
|
mime_parser.has_chat_version(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -258,12 +255,14 @@ pub(crate) async fn dc_receive_imf_inner(
|
|||||||
if !created_db_entries.is_empty() {
|
if !created_db_entries.is_empty() {
|
||||||
if needs_delete_job || delete_server_after == Some(0) {
|
if needs_delete_job || delete_server_after == Some(0) {
|
||||||
for db_entry in &created_db_entries {
|
for db_entry in &created_db_entries {
|
||||||
info!(context, "verbose (issue 2335): adding job after receive");
|
|
||||||
let mut params = Params::new();
|
|
||||||
params.set(Param::Arg, "comment: verbose (issue 2335) dc_receive_imf()");
|
|
||||||
job::add(
|
job::add(
|
||||||
context,
|
context,
|
||||||
job::Job::new(Action::DeleteMsgOnImap, db_entry.1.to_u32(), params, 0),
|
job::Job::new(
|
||||||
|
Action::DeleteMsgOnImap,
|
||||||
|
db_entry.1.to_u32(),
|
||||||
|
Params::new(),
|
||||||
|
0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -364,7 +363,7 @@ async fn add_parts(
|
|||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
incoming: bool,
|
incoming: bool,
|
||||||
incoming_origin: Origin,
|
incoming_origin: Origin,
|
||||||
server_folder: impl AsRef<str>,
|
server_folder: &str,
|
||||||
server_uid: u32,
|
server_uid: u32,
|
||||||
to_ids: &ContactIds,
|
to_ids: &ContactIds,
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
@@ -392,9 +391,8 @@ async fn add_parts(
|
|||||||
if let Some((old_server_folder, old_server_uid, _)) =
|
if let Some((old_server_folder, old_server_uid, _)) =
|
||||||
message::rfc724_mid_exists(context, rfc724_mid).await?
|
message::rfc724_mid_exists(context, rfc724_mid).await?
|
||||||
{
|
{
|
||||||
if old_server_folder != server_folder.as_ref() || old_server_uid != server_uid {
|
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||||
message::update_server_uid(context, rfc724_mid, server_folder.as_ref(), server_uid)
|
message::update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!(context, "Message already in DB");
|
warn!(context, "Message already in DB");
|
||||||
@@ -472,10 +470,9 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (test_normal_chat_id, test_normal_chat_id_blocked) =
|
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id)
|
||||||
chat::lookup_by_contact_id(context, from_id)
|
.await
|
||||||
.await
|
.unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
|
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
|
||||||
// it might also be blocked and displayed in the deaddrop as a result
|
// it might also be blocked and displayed in the deaddrop as a result
|
||||||
@@ -496,17 +493,18 @@ async fn add_parts(
|
|||||||
if chat_id.is_unset() {
|
if chat_id.is_unset() {
|
||||||
// try to create a group
|
// try to create a group
|
||||||
|
|
||||||
let create_blocked =
|
let create_blocked = match test_normal_chat {
|
||||||
if !test_normal_chat_id.is_unset() && test_normal_chat_id_blocked == Blocked::Not {
|
Some(ChatIdBlocked {
|
||||||
Blocked::Not
|
id: _,
|
||||||
} else {
|
blocked: Blocked::Not,
|
||||||
Blocked::Deaddrop
|
}) => Blocked::Not,
|
||||||
};
|
_ => Blocked::Deaddrop,
|
||||||
|
};
|
||||||
|
|
||||||
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
||||||
context,
|
context,
|
||||||
&mut mime_parser,
|
&mut mime_parser,
|
||||||
if test_normal_chat_id.is_unset() {
|
if test_normal_chat.is_none() {
|
||||||
allow_creation
|
allow_creation
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
@@ -598,16 +596,17 @@ async fn add_parts(
|
|||||||
Blocked::Deaddrop
|
Blocked::Deaddrop
|
||||||
};
|
};
|
||||||
|
|
||||||
if !test_normal_chat_id.is_unset() {
|
if let Some(chat) = test_normal_chat {
|
||||||
*chat_id = test_normal_chat_id;
|
*chat_id = chat.id;
|
||||||
chat_id_blocked = test_normal_chat_id_blocked;
|
chat_id_blocked = chat.blocked;
|
||||||
} else if allow_creation {
|
} else if allow_creation {
|
||||||
let (id, bl) =
|
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
|
||||||
chat::create_or_lookup_by_contact_id(context, from_id, create_blocked)
|
.await
|
||||||
.await
|
.log_err(context, "Failed to get (new) chat for contact")
|
||||||
.unwrap_or_default();
|
{
|
||||||
*chat_id = id;
|
*chat_id = chat.id;
|
||||||
chat_id_blocked = bl;
|
chat_id_blocked = chat.blocked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
||||||
if Blocked::Not == create_blocked {
|
if Blocked::Not == create_blocked {
|
||||||
@@ -652,7 +651,7 @@ async fn add_parts(
|
|||||||
let is_spam = (chat_id_blocked == Blocked::Deaddrop)
|
let is_spam = (chat_id_blocked == Blocked::Deaddrop)
|
||||||
&& !incoming_origin.is_known()
|
&& !incoming_origin.is_known()
|
||||||
&& (is_dc_message == MessengerMessage::No)
|
&& (is_dc_message == MessengerMessage::No)
|
||||||
&& context.is_spam_folder(&server_folder).await?;
|
&& context.is_spam_folder(server_folder).await?;
|
||||||
if is_spam {
|
if is_spam {
|
||||||
*chat_id = DC_CHAT_ID_TRASH;
|
*chat_id = DC_CHAT_ID_TRASH;
|
||||||
info!(context, "Message is probably spam (TRASH)");
|
info!(context, "Message is probably spam (TRASH)");
|
||||||
@@ -686,7 +685,7 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.is_sentbox(&server_folder).await?
|
if !context.is_sentbox(server_folder).await?
|
||||||
&& mime_parser.get(HeaderDef::Received).is_none()
|
&& mime_parser.get(HeaderDef::Received).is_none()
|
||||||
{
|
{
|
||||||
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
|
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
|
||||||
@@ -723,11 +722,12 @@ async fn add_parts(
|
|||||||
} else {
|
} else {
|
||||||
Blocked::Deaddrop
|
Blocked::Deaddrop
|
||||||
};
|
};
|
||||||
let (id, bl) = chat::create_or_lookup_by_contact_id(context, to_id, create_blocked)
|
if let Ok(chat) =
|
||||||
.await
|
ChatIdBlocked::get_for_contact(context, to_id, create_blocked).await
|
||||||
.unwrap_or_default();
|
{
|
||||||
*chat_id = id;
|
*chat_id = chat.id;
|
||||||
chat_id_blocked = bl;
|
chat_id_blocked = chat.blocked;
|
||||||
|
}
|
||||||
|
|
||||||
if !chat_id.is_unset()
|
if !chat_id.is_unset()
|
||||||
&& Blocked::Not != chat_id_blocked
|
&& Blocked::Not != chat_id_blocked
|
||||||
@@ -745,12 +745,14 @@ async fn add_parts(
|
|||||||
if chat_id.is_unset() && self_sent {
|
if chat_id.is_unset() && self_sent {
|
||||||
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
||||||
// maybe an Autocrypt Setup Message
|
// maybe an Autocrypt Setup Message
|
||||||
let (id, bl) =
|
if let Ok(chat) =
|
||||||
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
|
ChatIdBlocked::get_for_contact(context, DC_CONTACT_ID_SELF, Blocked::Not)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.log_err(context, "Failed to get (new) chat for contact")
|
||||||
*chat_id = id;
|
{
|
||||||
chat_id_blocked = bl;
|
*chat_id = chat.id;
|
||||||
|
chat_id_blocked = chat.blocked;
|
||||||
|
}
|
||||||
|
|
||||||
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
||||||
chat_id.unblock(context).await;
|
chat_id.unblock(context).await;
|
||||||
@@ -913,7 +915,7 @@ async fn add_parts(
|
|||||||
|
|
||||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||||
|
|
||||||
let server_folder = server_folder.as_ref();
|
let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new());
|
||||||
let is_system_message = mime_parser.is_system_message;
|
let is_system_message = mime_parser.is_system_message;
|
||||||
|
|
||||||
// if indicated by the parser,
|
// if indicated by the parser,
|
||||||
@@ -925,25 +927,59 @@ async fn add_parts(
|
|||||||
|
|
||||||
let mime_headers = if save_mime_headers || save_mime_modified {
|
let mime_headers = if save_mime_headers || save_mime_modified {
|
||||||
if mime_parser.was_encrypted() && !mime_parser.decoded_data.is_empty() {
|
if mime_parser.was_encrypted() && !mime_parser.decoded_data.is_empty() {
|
||||||
String::from_utf8_lossy(&mime_parser.decoded_data)
|
String::from_utf8_lossy(&mime_parser.decoded_data).to_string()
|
||||||
} else {
|
} else {
|
||||||
String::from_utf8_lossy(imf_raw)
|
String::from_utf8_lossy(imf_raw).to_string()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"".into()
|
"".into()
|
||||||
};
|
};
|
||||||
|
|
||||||
for part in &mut mime_parser.parts {
|
let sent_timestamp = *sent_timestamp;
|
||||||
|
let is_hidden = *hidden;
|
||||||
|
let chat_id = *chat_id;
|
||||||
|
|
||||||
|
// TODO: can this clone be avoided?
|
||||||
|
let rfc724_mid = rfc724_mid.to_string();
|
||||||
|
|
||||||
|
let mut is_hidden = is_hidden;
|
||||||
|
let mut ids = Vec::with_capacity(parts.len());
|
||||||
|
|
||||||
|
let conn = context.sql.get_conn().await?;
|
||||||
|
|
||||||
|
for part in &mut parts {
|
||||||
let mut txt_raw = "".to_string();
|
let mut txt_raw = "".to_string();
|
||||||
|
let mut stmt = conn.prepare_cached(
|
||||||
|
r#"
|
||||||
|
INSERT INTO msgs
|
||||||
|
(
|
||||||
|
rfc724_mid, server_folder, server_uid, chat_id,
|
||||||
|
from_id, to_id, timestamp, timestamp_sent,
|
||||||
|
timestamp_rcvd, type, state, msgrmsg,
|
||||||
|
txt, subject, txt_raw, param,
|
||||||
|
bytes, hidden, mime_headers, mime_in_reply_to,
|
||||||
|
mime_references, mime_modified, error, ephemeral_timer,
|
||||||
|
ephemeral_timestamp
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?,
|
||||||
|
?
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
let is_location_kml =
|
let is_location_kml =
|
||||||
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
|
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
|
||||||
|
|
||||||
if is_mdn || is_location_kml {
|
if is_mdn || is_location_kml {
|
||||||
*hidden = true;
|
is_hidden = true;
|
||||||
if incoming {
|
if incoming {
|
||||||
// Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message
|
state = MessageState::InSeen; // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message
|
||||||
state = MessageState::InSeen;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,78 +1010,61 @@ async fn add_parts(
|
|||||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||||
let trash = chat_id.is_trash();
|
let trash = chat_id.is_trash();
|
||||||
|
|
||||||
let row_id = context
|
stmt.execute(paramsv![
|
||||||
.sql
|
rfc724_mid,
|
||||||
.insert(
|
server_folder,
|
||||||
sqlx::query(
|
server_uid as i32,
|
||||||
r#"
|
chat_id,
|
||||||
INSERT INTO msgs
|
if trash { 0 } else { from_id as i32 },
|
||||||
(
|
if trash { 0 } else { to_id as i32 },
|
||||||
rfc724_mid, server_folder, server_uid, chat_id,
|
sort_timestamp,
|
||||||
from_id, to_id, timestamp, timestamp_sent,
|
sent_timestamp,
|
||||||
timestamp_rcvd, type, state, msgrmsg,
|
rcvd_timestamp,
|
||||||
txt, subject, txt_raw, param,
|
part.typ,
|
||||||
bytes, hidden, mime_headers, mime_in_reply_to,
|
state,
|
||||||
mime_references, mime_modified, error, ephemeral_timer,
|
is_dc_message,
|
||||||
ephemeral_timestamp
|
if trash { "" } else { &part.msg },
|
||||||
)
|
if trash { "" } else { &subject },
|
||||||
VALUES (
|
// txt_raw might contain invalid utf8
|
||||||
?, ?, ?, ?,
|
if trash { "" } else { &txt_raw },
|
||||||
?, ?, ?, ?,
|
if trash {
|
||||||
?, ?, ?, ?,
|
"".to_string()
|
||||||
?, ?, ?, ?,
|
} else {
|
||||||
?, ?, ?, ?,
|
part.param.to_string()
|
||||||
?, ?, ?, ?,
|
},
|
||||||
?
|
part.bytes as isize,
|
||||||
);
|
is_hidden,
|
||||||
"#,
|
if (save_mime_headers || mime_modified) && !trash {
|
||||||
)
|
mime_headers.to_string()
|
||||||
.bind(rfc724_mid)
|
} else {
|
||||||
.bind(server_folder)
|
"".to_string()
|
||||||
.bind(server_uid as i32)
|
},
|
||||||
.bind(*chat_id)
|
mime_in_reply_to,
|
||||||
.bind(if trash { 0 } else { from_id as i32 })
|
mime_references,
|
||||||
.bind(if trash { 0 } else { to_id as i32 })
|
mime_modified,
|
||||||
.bind(sort_timestamp)
|
part.error.take().unwrap_or_default(),
|
||||||
.bind(*sent_timestamp)
|
ephemeral_timer,
|
||||||
.bind(rcvd_timestamp)
|
ephemeral_timestamp
|
||||||
.bind(part.typ)
|
])?;
|
||||||
.bind(state)
|
let row_id = conn.last_insert_rowid();
|
||||||
.bind(is_dc_message)
|
|
||||||
.bind(if trash { "" } else { &part.msg })
|
|
||||||
.bind(if trash { "" } else { &subject })
|
|
||||||
// txt_raw might contain invalid utf8
|
|
||||||
.bind(if trash { "" } else { &txt_raw })
|
|
||||||
.bind(if trash {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
|
||||||
part.param.to_string()
|
|
||||||
})
|
|
||||||
.bind(part.bytes as i64)
|
|
||||||
.bind(*hidden)
|
|
||||||
.bind(if (save_mime_headers || mime_modified) && !trash {
|
|
||||||
mime_headers.to_string()
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
})
|
|
||||||
.bind(&mime_in_reply_to)
|
|
||||||
.bind(&mime_references)
|
|
||||||
.bind(&mime_modified)
|
|
||||||
.bind(part.error.take().unwrap_or_default())
|
|
||||||
.bind(ephemeral_timer)
|
|
||||||
.bind(ephemeral_timestamp),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
|
||||||
|
|
||||||
created_db_entries.push((*chat_id, msg_id));
|
drop(stmt);
|
||||||
*insert_msg_id = msg_id;
|
ids.push(MsgId::new(u32::try_from(row_id)?));
|
||||||
|
}
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
if let Some(id) = ids.iter().last() {
|
||||||
|
*insert_msg_id = *id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*hidden {
|
if !is_hidden {
|
||||||
chat_id.unarchive(context).await?;
|
chat_id.unarchive(context).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*hidden = is_hidden;
|
||||||
|
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
|
||||||
|
mime_parser.parts = parts;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
|
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
|
||||||
@@ -1053,7 +1072,7 @@ INSERT INTO msgs
|
|||||||
|
|
||||||
// new outgoing message from another device marks the chat as noticed.
|
// new outgoing message from another device marks the chat as noticed.
|
||||||
if !incoming && !*hidden && !chat_id.is_special() {
|
if !incoming && !*hidden && !chat_id.is_special() {
|
||||||
chat::marknoticed_chat_if_older_than(context, *chat_id, sort_timestamp).await?;
|
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check event to send
|
// check event to send
|
||||||
@@ -1083,7 +1102,7 @@ INSERT INTO msgs
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
if !is_mdn {
|
if !is_mdn {
|
||||||
update_last_subject(context, *chat_id, mime_parser)
|
update_last_subject(context, chat_id, mime_parser)
|
||||||
.await
|
.await
|
||||||
.ok_or_log_msg(context, "Could not update LastSubject of chat");
|
.ok_or_log_msg(context, "Could not update LastSubject of chat");
|
||||||
}
|
}
|
||||||
@@ -1165,9 +1184,8 @@ async fn calc_sort_timestamp(
|
|||||||
let last_msg_time: Option<i64> = context
|
let last_msg_time: Option<i64> = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?")
|
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
|
||||||
.bind(chat_id)
|
paramsv![chat_id, MessageState::InFresh],
|
||||||
.bind(MessageState::InFresh),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -1479,9 +1497,8 @@ async fn create_or_lookup_group(
|
|||||||
if context
|
if context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE chats SET name=? WHERE id=?;")
|
"UPDATE chats SET name=? WHERE id=?;",
|
||||||
.bind(grpname.to_string())
|
paramsv![grpname.to_string(), chat_id],
|
||||||
.bind(chat_id),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -1518,7 +1535,10 @@ async fn create_or_lookup_group(
|
|||||||
// start from scratch.
|
// start from scratch.
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM chats_contacts WHERE chat_id=?;").bind(chat_id))
|
.execute(
|
||||||
|
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||||
|
paramsv![chat_id],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
@@ -1762,14 +1782,15 @@ async fn create_multiuser_record(
|
|||||||
) -> Result<ChatId> {
|
) -> Result<ChatId> {
|
||||||
let row_id =
|
let row_id =
|
||||||
context.sql.insert(
|
context.sql.insert(
|
||||||
sqlx::query(
|
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);",
|
||||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);")
|
paramsv![
|
||||||
.bind(chattype)
|
chattype,
|
||||||
.bind(grpname.as_ref())
|
grpname.as_ref(),
|
||||||
.bind(grpid.as_ref())
|
grpid.as_ref(),
|
||||||
.bind(create_blocked)
|
create_blocked,
|
||||||
.bind(time())
|
time(),
|
||||||
.bind(create_protected)
|
create_protected,
|
||||||
|
],
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||||
@@ -1803,31 +1824,34 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<St
|
|||||||
.unwrap_or_else(|| "no-self".to_string())
|
.unwrap_or_else(|| "no-self".to_string())
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
let q = format!(
|
let members = context
|
||||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
.sql
|
||||||
member_ids_str
|
.query_map(
|
||||||
);
|
format!(
|
||||||
|
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
||||||
let mut members = member_cs;
|
member_ids_str
|
||||||
|
),
|
||||||
if let Ok(rows) = context.sql.fetch(sqlx::query(&q)).await {
|
paramsv![],
|
||||||
let mut addrs = rows
|
|row| row.get::<_, String>(0),
|
||||||
.map(|row| row?.try_get::<String, _>(0))
|
|rows| {
|
||||||
.collect::<sqlx::Result<Vec<_>>>()
|
let mut addrs = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
.await?;
|
addrs.sort();
|
||||||
addrs.sort();
|
let mut acc = member_cs.clone();
|
||||||
for addr in &addrs {
|
for addr in &addrs {
|
||||||
members += ",";
|
acc += ",";
|
||||||
members += &addr.to_lowercase();
|
acc += &addr.to_lowercase();
|
||||||
}
|
}
|
||||||
}
|
Ok(acc)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(hex_hash(&members))
|
Ok(hex_hash(&members))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::indexing_slicing)]
|
#[allow(clippy::indexing_slicing)]
|
||||||
fn hex_hash(s: impl AsRef<str>) -> String {
|
fn hex_hash(s: &str) -> String {
|
||||||
let bytes = s.as_ref().as_bytes();
|
let bytes = s.as_bytes();
|
||||||
let result = Sha256::digest(bytes);
|
let result = Sha256::digest(bytes);
|
||||||
hex::encode(&result[..8])
|
hex::encode(&result[..8])
|
||||||
}
|
}
|
||||||
@@ -1887,26 +1911,34 @@ async fn check_verified_properties(
|
|||||||
}
|
}
|
||||||
let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ",");
|
let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ",");
|
||||||
|
|
||||||
let q = format!(
|
let rows = context
|
||||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
.sql
|
||||||
|
.query_map(
|
||||||
|
format!(
|
||||||
|
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||||
to_ids_str
|
to_ids_str
|
||||||
);
|
),
|
||||||
|
paramsv![],
|
||||||
let mut rows = context.sql.fetch(sqlx::query(&q)).await?;
|
|row| {
|
||||||
|
let to_addr: String = row.get(0)?;
|
||||||
while let Some(row) = rows.next().await {
|
let is_verified: i32 = row.get(1).unwrap_or(0);
|
||||||
let row = row?;
|
Ok((to_addr, is_verified != 0))
|
||||||
let to_addr: String = row.try_get(0)?;
|
},
|
||||||
let mut is_verified = row.try_get::<i32, _>(1)? != 0;
|
|rows| {
|
||||||
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (to_addr, mut is_verified) in rows.into_iter() {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"check_verified_properties: {:?} self={:?}",
|
"check_verified_properties: {:?} self={:?}",
|
||||||
to_addr,
|
to_addr,
|
||||||
context.is_self_addr(&to_addr).await
|
context.is_self_addr(&to_addr).await
|
||||||
);
|
);
|
||||||
|
|
||||||
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
||||||
|
|
||||||
// mark gossiped keys (if any) as verified
|
// mark gossiped keys (if any) as verified
|
||||||
@@ -2052,7 +2084,7 @@ async fn add_or_lookup_contact_by_addr(
|
|||||||
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
|
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
|
||||||
|
|
||||||
let (row_id, _modified) =
|
let (row_id, _modified) =
|
||||||
Contact::add_or_lookup(context, display_name_normalized, addr, origin).await?;
|
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
|
||||||
ensure!(row_id > 0, "could not add contact: {:?}", addr);
|
ensure!(row_id > 0, "could not add contact: {:?}", addr);
|
||||||
|
|
||||||
Ok(row_id)
|
Ok(row_id)
|
||||||
@@ -2321,7 +2353,7 @@ mod tests {
|
|||||||
let t = TestContext::new_alice().await;
|
let t = TestContext::new_alice().await;
|
||||||
|
|
||||||
let bob_id = Contact::create(&t, "bob", "bob@example.com").await.unwrap();
|
let bob_id = Contact::create(&t, "bob", "bob@example.com").await.unwrap();
|
||||||
let one2one_id = chat::create_by_contact_id(&t, bob_id).await.unwrap();
|
let one2one_id = ChatId::create_for_contact(&t, bob_id).await.unwrap();
|
||||||
one2one_id
|
one2one_id
|
||||||
.set_visibility(&t, ChatVisibility::Archived)
|
.set_visibility(&t, ChatVisibility::Archived)
|
||||||
.await
|
.await
|
||||||
@@ -2490,7 +2522,7 @@ mod tests {
|
|||||||
let contact_id = Contact::create(&t, "foobar", "foobar@example.com")
|
let contact_id = Contact::create(&t, "foobar", "foobar@example.com")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let chat_id = chat::create_by_contact_id(&t, contact_id).await.unwrap();
|
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
|
||||||
dc_receive_imf(
|
dc_receive_imf(
|
||||||
&t,
|
&t,
|
||||||
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||||
|
|||||||
@@ -632,10 +632,17 @@ impl FromStr for EmailAddress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
||||||
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
|
pub(crate) fn improve_single_line_input(input: &str) -> String {
|
||||||
input
|
input
|
||||||
.as_ref()
|
|
||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
.replace("\r", " ")
|
.replace("\r", " ")
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
215
src/ephemeral.rs
215
src/ephemeral.rs
@@ -61,25 +61,21 @@ use std::num::ParseIntError;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::{ensure, Context as _, Error};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
use async_std::task;
|
use async_std::task;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
|
use crate::chat::{send_msg, ChatId};
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||||
};
|
};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::time;
|
use crate::dc_tools::time;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
|
use crate::job;
|
||||||
use crate::message::{Message, MessageState, MsgId};
|
use crate::message::{Message, MessageState, MsgId};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::sql;
|
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::{
|
|
||||||
chat::{lookup_by_contact_id, send_msg, ChatId},
|
|
||||||
job,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||||
pub enum Timer {
|
pub enum Timer {
|
||||||
@@ -124,51 +120,39 @@ impl FromStr for Timer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sqlx::Type<sqlx::Sqlite> for Timer {
|
impl rusqlite::types::ToSql for Timer {
|
||||||
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
<i64 as sqlx::Type<_>>::type_info()
|
let val = rusqlite::types::Value::Integer(match self {
|
||||||
}
|
Self::Disabled => 0,
|
||||||
|
Self::Enabled { duration } => i64::from(*duration),
|
||||||
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
|
});
|
||||||
<i64 as sqlx::Type<_>>::compatible(ty)
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer {
|
impl rusqlite::types::FromSql for Timer {
|
||||||
fn encode_by_ref(
|
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||||
&self,
|
i64::column_result(value).and_then(|value| {
|
||||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
if value == 0 {
|
||||||
) -> sqlx::encode::IsNull {
|
Ok(Self::Disabled)
|
||||||
args.push(sqlx::sqlite::SqliteArgumentValue::Int64(
|
} else if let Ok(duration) = u32::try_from(value) {
|
||||||
self.to_u32() as i64
|
Ok(Self::Enabled { duration })
|
||||||
));
|
} else {
|
||||||
|
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
||||||
sqlx::encode::IsNull::No
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer {
|
|
||||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
|
|
||||||
let value: i64 = sqlx::Decode::decode(value)?;
|
|
||||||
if value == 0 {
|
|
||||||
Ok(Self::Disabled)
|
|
||||||
} else if let Ok(duration) = u32::try_from(value) {
|
|
||||||
Ok(Self::Enabled { duration })
|
|
||||||
} else {
|
|
||||||
Err(Box::new(sqlx::Error::Decode(Box::new(
|
|
||||||
crate::error::OutOfRangeError,
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatId {
|
impl ChatId {
|
||||||
/// Get ephemeral message timer value in seconds.
|
/// Get ephemeral message timer value in seconds.
|
||||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||||
let timer = context
|
let timer = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self),
|
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||||
|
paramsv![self],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(timer.unwrap_or_default())
|
Ok(timer.unwrap_or_default())
|
||||||
@@ -182,19 +166,16 @@ impl ChatId {
|
|||||||
self,
|
self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
timer: Timer,
|
timer: Timer,
|
||||||
) -> Result<(), Error> {
|
) -> Result<()> {
|
||||||
ensure!(!self.is_special(), "Invalid chat ID");
|
ensure!(!self.is_special(), "Invalid chat ID");
|
||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE chats
|
||||||
"UPDATE chats
|
|
||||||
SET ephemeral_timer=?
|
SET ephemeral_timer=?
|
||||||
WHERE id=?;",
|
WHERE id=?;",
|
||||||
)
|
paramsv![timer, self],
|
||||||
.bind(timer)
|
|
||||||
.bind(self),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -208,7 +189,7 @@ impl ChatId {
|
|||||||
/// Set ephemeral message timer value in seconds.
|
/// Set ephemeral message timer value in seconds.
|
||||||
///
|
///
|
||||||
/// If timer value is 0, disable ephemeral message timer.
|
/// If timer value is 0, disable ephemeral message timer.
|
||||||
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
|
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<()> {
|
||||||
if timer == self.get_ephemeral_timer(context).await? {
|
if timer == self.get_ephemeral_timer(context).await? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -233,45 +214,44 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
|||||||
from_id: u32,
|
from_id: u32,
|
||||||
) -> String {
|
) -> String {
|
||||||
match timer {
|
match timer {
|
||||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
|
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||||
Timer::Enabled { duration } => match duration {
|
Timer::Enabled { duration } => match duration {
|
||||||
0..=59 => {
|
0..=59 => {
|
||||||
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32)
|
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await,
|
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||||
61..=3599 => {
|
61..=3599 => {
|
||||||
stock_str::msg_ephemeral_timer_minutes(
|
stock_str::msg_ephemeral_timer_minutes(
|
||||||
context,
|
context,
|
||||||
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
||||||
from_id as u32,
|
from_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await,
|
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
|
||||||
3601..=86399 => {
|
3601..=86399 => {
|
||||||
stock_str::msg_ephemeral_timer_hours(
|
stock_str::msg_ephemeral_timer_hours(
|
||||||
context,
|
context,
|
||||||
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
||||||
from_id as u32,
|
from_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await,
|
86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
|
||||||
86401..=604_799 => {
|
86401..=604_799 => {
|
||||||
stock_str::msg_ephemeral_timer_days(
|
stock_str::msg_ephemeral_timer_days(
|
||||||
context,
|
context,
|
||||||
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
||||||
from_id as u32,
|
from_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await,
|
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
|
||||||
_ => {
|
_ => {
|
||||||
stock_str::msg_ephemeral_timer_weeks(
|
stock_str::msg_ephemeral_timer_weeks(
|
||||||
context,
|
context,
|
||||||
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
||||||
from_id as u32,
|
from_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -284,15 +264,14 @@ impl MsgId {
|
|||||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||||
let res = match context
|
let res = match context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value::<_, i64>(
|
.query_get_value(
|
||||||
sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self),
|
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||||
|
paramsv![self],
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
None | Some(0) => Timer::Disabled,
|
None | Some(0) => Timer::Disabled,
|
||||||
Some(duration) => Timer::Enabled {
|
Some(duration) => Timer::Enabled { duration },
|
||||||
duration: u32::try_from(duration)?,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
@@ -305,14 +284,10 @@ impl MsgId {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
|
||||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||||
AND id = ?",
|
AND id = ?",
|
||||||
)
|
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||||
.bind(ephemeral_timestamp)
|
|
||||||
.bind(ephemeral_timestamp)
|
|
||||||
.bind(self),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
schedule_ephemeral_task(context).await;
|
schedule_ephemeral_task(context).await;
|
||||||
@@ -329,14 +304,13 @@ impl MsgId {
|
|||||||
/// false. This function does not emit the MsgsChanged event itself,
|
/// false. This function does not emit the MsgsChanged event itself,
|
||||||
/// because it is also called when chatlist is reloaded, and emitting
|
/// because it is also called when chatlist is reloaded, and emitting
|
||||||
/// MsgsChanged there will cause infinite reload loop.
|
/// MsgsChanged there will cause infinite reload loop.
|
||||||
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
|
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool> {
|
||||||
let mut updated = context
|
let mut updated = context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
// If you change which information is removed here, also change MsgId::trash() and
|
||||||
// If you change which information is removed here, also change MsgId::trash() and
|
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||||
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
r#"
|
||||||
r#"
|
|
||||||
UPDATE msgs
|
UPDATE msgs
|
||||||
SET
|
SET
|
||||||
chat_id=?, txt='', subject='', txt_raw='',
|
chat_id=?, txt='', subject='', txt_raw='',
|
||||||
@@ -346,24 +320,19 @@ WHERE
|
|||||||
AND ephemeral_timestamp <= ?
|
AND ephemeral_timestamp <= ?
|
||||||
AND chat_id != ?
|
AND chat_id != ?
|
||||||
"#,
|
"#,
|
||||||
)
|
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||||
.bind(DC_CHAT_ID_TRASH)
|
|
||||||
.bind(time())
|
|
||||||
.bind(DC_CHAT_ID_TRASH),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("update failed")?
|
.context("update failed")?
|
||||||
> 0;
|
> 0;
|
||||||
|
|
||||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
|
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
|
||||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
let self_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
|
||||||
.await
|
.await?
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
.0;
|
let device_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
|
||||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
.await?
|
||||||
.await
|
.unwrap_or_default();
|
||||||
.unwrap_or_default()
|
|
||||||
.0;
|
|
||||||
|
|
||||||
let threshold_timestamp = time() - delete_device_after;
|
let threshold_timestamp = time() - delete_device_after;
|
||||||
|
|
||||||
@@ -374,19 +343,19 @@ WHERE
|
|||||||
let rows_modified = context
|
let rows_modified = context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE msgs \
|
||||||
"UPDATE msgs \
|
|
||||||
SET txt = 'DELETED', chat_id = ? \
|
SET txt = 'DELETED', chat_id = ? \
|
||||||
WHERE timestamp < ? \
|
WHERE timestamp < ? \
|
||||||
AND chat_id > ? \
|
AND chat_id > ? \
|
||||||
AND chat_id != ? \
|
AND chat_id != ? \
|
||||||
AND chat_id != ?",
|
AND chat_id != ?",
|
||||||
)
|
paramsv![
|
||||||
.bind(DC_CHAT_ID_TRASH)
|
DC_CHAT_ID_TRASH,
|
||||||
.bind(threshold_timestamp)
|
threshold_timestamp,
|
||||||
.bind(DC_CHAT_ID_LAST_SPECIAL)
|
DC_CHAT_ID_LAST_SPECIAL,
|
||||||
.bind(self_chat_id)
|
self_chat_id,
|
||||||
.bind(device_chat_id),
|
device_chat_id
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("deleted update failed")?;
|
.context("deleted update failed")?;
|
||||||
@@ -412,8 +381,7 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
|||||||
let ephemeral_timestamp: Option<i64> = match context
|
let ephemeral_timestamp: Option<i64> = match context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query(
|
r#"
|
||||||
r#"
|
|
||||||
SELECT ephemeral_timestamp
|
SELECT ephemeral_timestamp
|
||||||
FROM msgs
|
FROM msgs
|
||||||
WHERE ephemeral_timestamp != 0
|
WHERE ephemeral_timestamp != 0
|
||||||
@@ -421,8 +389,7 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
|||||||
ORDER BY ephemeral_timestamp ASC
|
ORDER BY ephemeral_timestamp ASC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
"#,
|
"#,
|
||||||
)
|
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||||
.bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -475,7 +442,7 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
|||||||
///
|
///
|
||||||
/// It looks up the trash chat too, to find messages that are already
|
/// It looks up the trash chat too, to find messages that are already
|
||||||
/// deleted locally, but not deleted on the server.
|
/// deleted locally, but not deleted on the server.
|
||||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
let threshold_timestamp = match context.get_config_delete_server_after().await? {
|
let threshold_timestamp = match context.get_config_delete_server_after().await? {
|
||||||
@@ -483,11 +450,10 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
|
|||||||
Some(delete_server_after) => now - delete_server_after,
|
Some(delete_server_after) => now - delete_server_after,
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch_optional(
|
.query_row_optional(
|
||||||
sqlx::query(
|
"SELECT id FROM msgs \
|
||||||
"SELECT id FROM msgs \
|
|
||||||
WHERE ( \
|
WHERE ( \
|
||||||
timestamp < ? \
|
timestamp < ? \
|
||||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||||
@@ -495,19 +461,13 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
|
|||||||
AND server_uid != 0 \
|
AND server_uid != 0 \
|
||||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||||
LIMIT 1",
|
LIMIT 1",
|
||||||
)
|
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
|
||||||
.bind(threshold_timestamp)
|
|row| {
|
||||||
.bind(now)
|
let msg_id: MsgId = row.get(0)?;
|
||||||
.bind(job::Action::DeleteMsgOnImap),
|
Ok(msg_id)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
|
||||||
if let Some(row) = row {
|
|
||||||
let msg_id = row.try_get(0)?;
|
|
||||||
Ok(Some(msg_id))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start ephemeral timers for seen messages if they are not started
|
/// Start ephemeral timers for seen messages if they are not started
|
||||||
@@ -519,21 +479,21 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
|
|||||||
///
|
///
|
||||||
/// This function is supposed to be called in the background,
|
/// This function is supposed to be called in the background,
|
||||||
/// e.g. from housekeeping task.
|
/// e.g. from housekeeping task.
|
||||||
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
|
pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE msgs \
|
||||||
"UPDATE msgs \
|
|
||||||
SET ephemeral_timestamp = ? + ephemeral_timer \
|
SET ephemeral_timestamp = ? + ephemeral_timer \
|
||||||
WHERE ephemeral_timer > 0 \
|
WHERE ephemeral_timer > 0 \
|
||||||
AND ephemeral_timestamp = 0 \
|
AND ephemeral_timestamp = 0 \
|
||||||
AND state NOT IN (?, ?, ?)",
|
AND state NOT IN (?, ?, ?)",
|
||||||
)
|
paramsv![
|
||||||
.bind(time())
|
time(),
|
||||||
.bind(MessageState::InFresh)
|
MessageState::InFresh,
|
||||||
.bind(MessageState::InNoticed)
|
MessageState::InNoticed,
|
||||||
.bind(MessageState::OutDraft),
|
MessageState::OutDraft
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -770,7 +730,10 @@ mod tests {
|
|||||||
// Check that the msg will be deleted on the server
|
// Check that the msg will be deleted on the server
|
||||||
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
||||||
t.sql
|
t.sql
|
||||||
.execute(sqlx::query("UPDATE msgs SET server_uid=1 WHERE id=?").bind(msg.sender_msg_id))
|
.execute(
|
||||||
|
"UPDATE msgs SET server_uid=1 WHERE id=?",
|
||||||
|
paramsv![msg.sender_msg_id],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
||||||
@@ -808,7 +771,7 @@ mod tests {
|
|||||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||||
let rawtxt: Option<String> = t
|
let rawtxt: Option<String> = t
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id))
|
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
//! # Error handling
|
//! # Error handling
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
#[error("Out of Range")]
|
|
||||||
pub struct OutOfRangeError;
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ensure_eq {
|
macro_rules! ensure_eq {
|
||||||
($left:expr, $right:expr) => ({
|
($left:expr, $right:expr) => ({
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ pub enum EventType {
|
|||||||
/// - Messages sent, received or removed
|
/// - Messages sent, received or removed
|
||||||
/// - Chats created, deleted or archived
|
/// - Chats created, deleted or archived
|
||||||
/// - A draft has been set
|
/// - A draft has been set
|
||||||
|
///
|
||||||
|
/// The `chat_id` and `msg_id` values will be 0 if more than one message is changed.
|
||||||
#[strum(props(id = "2000"))]
|
#[strum(props(id = "2000"))]
|
||||||
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
||||||
|
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ test some special html-characters as < > and & but also " and &#x
|
|||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn test_get_html_empty() {
|
async fn test_get_html_empty() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let msg_id = MsgId::new_unset();
|
let msg_id = MsgId::new(100);
|
||||||
assert!(msg_id.get_html(&t).await.unwrap().is_none())
|
assert!(msg_id.get_html(&t).await.unwrap().is_none())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -521,29 +521,21 @@ impl Imap {
|
|||||||
// Write collected UIDs to SQLite database.
|
// Write collected UIDs to SQLite database.
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.transaction(|conn| {
|
.transaction(move |transaction| {
|
||||||
Box::pin(async move {
|
transaction.execute(
|
||||||
sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?")
|
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
|
||||||
.bind(&folder)
|
params![folder],
|
||||||
.execute(&mut *conn)
|
)?;
|
||||||
.await?;
|
for (uid, rfc724_mid) in &msg_ids {
|
||||||
|
// This may detect previously undetected moved
|
||||||
for (uid, rfc724_mid) in &msg_ids {
|
// messages, so we update server_folder too.
|
||||||
// This may detect previously undetected moved
|
transaction.execute(
|
||||||
// messages, so we update server_folder too.
|
"UPDATE msgs \
|
||||||
sqlx::query(
|
|
||||||
"UPDATE msgs \
|
|
||||||
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
|
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
|
||||||
)
|
params![folder, uid, rfc724_mid],
|
||||||
.bind(&folder)
|
)?;
|
||||||
.bind(uid)
|
}
|
||||||
.bind(rfc724_mid)
|
Ok(())
|
||||||
.execute(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -718,7 +710,7 @@ impl Imap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (largest_uid_processed, error_cnt) = self
|
let (largest_uid_processed, error_cnt) = self
|
||||||
.fetch_many_msgs(context, &folder, uids, fetch_existing_msgs)
|
.fetch_many_msgs(context, folder, uids, fetch_existing_msgs)
|
||||||
.await;
|
.await;
|
||||||
read_errors += error_cnt;
|
read_errors += error_cnt;
|
||||||
|
|
||||||
@@ -866,10 +858,10 @@ impl Imap {
|
|||||||
/// Fetches a list of messages by server UID.
|
/// Fetches a list of messages by server UID.
|
||||||
///
|
///
|
||||||
/// Returns the last uid fetch successfully and an error count.
|
/// Returns the last uid fetch successfully and an error count.
|
||||||
async fn fetch_many_msgs<S: AsRef<str>>(
|
async fn fetch_many_msgs(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: S,
|
folder: &str,
|
||||||
server_uids: Vec<u32>,
|
server_uids: Vec<u32>,
|
||||||
fetching_existing_messages: bool,
|
fetching_existing_messages: bool,
|
||||||
) -> (Option<u32>, usize) {
|
) -> (Option<u32>, usize) {
|
||||||
@@ -907,14 +899,14 @@ impl Imap {
|
|||||||
context,
|
context,
|
||||||
"Error on fetching messages #{} from folder \"{}\"; error={}.",
|
"Error on fetching messages #{} from folder \"{}\"; error={}.",
|
||||||
&set,
|
&set,
|
||||||
folder.as_ref(),
|
folder,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
return (None, server_uids.len());
|
return (None, server_uids.len());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let folder = folder.as_ref().to_string();
|
let folder = folder.to_string();
|
||||||
|
|
||||||
while let Some(Ok(msg)) = msgs.next().await {
|
while let Some(Ok(msg)) = msgs.next().await {
|
||||||
let server_uid = msg.uid.unwrap_or_default();
|
let server_uid = msg.uid.unwrap_or_default();
|
||||||
@@ -933,7 +925,11 @@ impl Imap {
|
|||||||
|
|
||||||
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
|
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
|
||||||
if is_deleted || msg.body().is_none() {
|
if is_deleted || msg.body().is_none() {
|
||||||
// No need to process these.
|
info!(
|
||||||
|
context,
|
||||||
|
"Not processing deleted or empty msg {}", server_uid
|
||||||
|
);
|
||||||
|
last_uid = Some(server_uid);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,7 +1133,7 @@ impl Imap {
|
|||||||
return Some(ImapActionResult::RetryLater);
|
return Some(ImapActionResult::RetryLater);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match self.select_folder(context, Some(&folder)).await {
|
match self.select_folder(context, Some(folder)).await {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
Err(select_folder::Error::ConnectionLost) => {
|
Err(select_folder::Error::ConnectionLost) => {
|
||||||
warn!(context, "Lost imap connection");
|
warn!(context, "Lost imap connection");
|
||||||
@@ -1732,15 +1728,9 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
|
||||||
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
|
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
|
||||||
)
|
paramsv![folder, 0u32, uid_next, uid_next, folder],
|
||||||
.bind(folder)
|
|
||||||
.bind(0i32)
|
|
||||||
.bind(uid_next as i64)
|
|
||||||
.bind(uid_next as i64)
|
|
||||||
.bind(folder),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1754,7 +1744,10 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
|||||||
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
|
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
|
||||||
Ok(context
|
Ok(context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder))
|
.query_get_value(
|
||||||
|
"SELECT uid_next FROM imap_sync WHERE folder=?;",
|
||||||
|
paramsv![folder],
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0))
|
.unwrap_or(0))
|
||||||
}
|
}
|
||||||
@@ -1767,15 +1760,9 @@ pub(crate) async fn set_uidvalidity(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
|
||||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
|
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
|
||||||
)
|
paramsv![folder, uidvalidity, 0u32, uidvalidity, folder],
|
||||||
.bind(folder)
|
|
||||||
.bind(uidvalidity as i32)
|
|
||||||
.bind(0i32)
|
|
||||||
.bind(uidvalidity as i32)
|
|
||||||
.bind(folder),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1785,7 +1772,8 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
|
|||||||
Ok(context
|
Ok(context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder),
|
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
|
||||||
|
paramsv![folder],
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or(0))
|
.unwrap_or(0))
|
||||||
@@ -32,10 +32,10 @@ impl DerefMut for Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
pub async fn login(
|
||||||
self,
|
self,
|
||||||
username: U,
|
username: &str,
|
||||||
password: P,
|
password: &str,
|
||||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||||
let Client { inner, is_secure } = self;
|
let Client { inner, is_secure } = self;
|
||||||
let session = inner
|
let session = inner
|
||||||
@@ -53,10 +53,10 @@ impl Client {
|
|||||||
Ok(Session { inner: session })
|
Ok(Session { inner: session })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
pub async fn authenticate(
|
||||||
self,
|
self,
|
||||||
auth_type: S,
|
auth_type: &str,
|
||||||
authenticator: A,
|
authenticator: impl async_imap::Authenticator,
|
||||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||||
let Client { inner, is_secure } = self;
|
let Client { inner, is_secure } = self;
|
||||||
let session =
|
let session =
|
||||||
@@ -75,15 +75,14 @@ impl Client {
|
|||||||
Ok(Session { inner: session })
|
Ok(Session { inner: session })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
|
pub async fn connect_secure(
|
||||||
addr: A,
|
addr: impl net::ToSocketAddrs,
|
||||||
domain: S,
|
domain: &str,
|
||||||
strict_tls: bool,
|
strict_tls: bool,
|
||||||
) -> ImapResult<Self> {
|
) -> ImapResult<Self> {
|
||||||
let stream = TcpStream::connect(addr).await?;
|
let stream = TcpStream::connect(addr).await?;
|
||||||
let tls = dc_build_tls(strict_tls);
|
let tls = dc_build_tls(strict_tls);
|
||||||
let tls_stream: Box<dyn SessionStream> =
|
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
|
||||||
Box::new(tls.connect(domain.as_ref(), stream).await?);
|
|
||||||
let mut client = ImapClient::new(tls_stream);
|
let mut client = ImapClient::new(tls_stream);
|
||||||
|
|
||||||
let _greeting = client
|
let _greeting = client
|
||||||
@@ -97,7 +96,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
|
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> ImapResult<Self> {
|
||||||
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||||
|
|
||||||
let mut client = ImapClient::new(stream);
|
let mut client = ImapClient::new(stream);
|
||||||
@@ -112,7 +111,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
|
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
|
||||||
if self.is_secure {
|
if self.is_secure {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +120,7 @@ impl Client {
|
|||||||
inner.run_command_and_check_ok("STARTTLS", None).await?;
|
inner.run_command_and_check_ok("STARTTLS", None).await?;
|
||||||
|
|
||||||
let stream = inner.into_inner();
|
let stream = inner.into_inner();
|
||||||
let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
|
let ssl_stream = tls.connect(domain, stream).await?;
|
||||||
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
|
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
|
||||||
|
|
||||||
Ok(Client {
|
Ok(Client {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl Imap {
|
|||||||
}
|
}
|
||||||
self.setup_handle(context).await?;
|
self.setup_handle(context).await?;
|
||||||
|
|
||||||
self.select_folder(context, watch_folder.clone()).await?;
|
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||||
|
|
||||||
let timeout = Duration::from_secs(23 * 60);
|
let timeout = Duration::from_secs(23 * 60);
|
||||||
let mut info = Default::default();
|
let mut info = Default::default();
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ impl Imap {
|
|||||||
/// select a folder, possibly update uid_validity and, if needed,
|
/// select a folder, possibly update uid_validity and, if needed,
|
||||||
/// expunge the folder to remove delete-marked messages.
|
/// expunge the folder to remove delete-marked messages.
|
||||||
/// Returns whether a new folder was selected.
|
/// Returns whether a new folder was selected.
|
||||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
pub(super) async fn select_folder(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: Option<S>,
|
folder: Option<&str>,
|
||||||
) -> Result<NewlySelected> {
|
) -> Result<NewlySelected> {
|
||||||
if self.session.is_none() {
|
if self.session.is_none() {
|
||||||
self.config.selected_folder = None;
|
self.config.selected_folder = None;
|
||||||
@@ -76,9 +76,9 @@ 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 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 there is _no_ new folder, we continue as we might want to expunge below.
|
||||||
if let Some(ref folder) = folder {
|
if let Some(folder) = folder {
|
||||||
if let Some(ref selected_folder) = self.config.selected_folder {
|
if let Some(ref selected_folder) = self.config.selected_folder {
|
||||||
if folder.as_ref() == selected_folder {
|
if folder == selected_folder {
|
||||||
return Ok(NewlySelected::No);
|
return Ok(NewlySelected::No);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ impl Imap {
|
|||||||
self.maybe_close_folder(context).await?;
|
self.maybe_close_folder(context).await?;
|
||||||
|
|
||||||
// select new folder
|
// select new folder
|
||||||
if let Some(ref folder) = folder {
|
if let Some(folder) = folder {
|
||||||
if let Some(ref mut session) = &mut self.session {
|
if let Some(ref mut session) = &mut self.session {
|
||||||
let res = session.select(folder).await;
|
let res = session.select(folder).await;
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ impl Imap {
|
|||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(mailbox) => {
|
Ok(mailbox) => {
|
||||||
self.config.selected_folder = Some(folder.as_ref().to_string());
|
self.config.selected_folder = Some(folder.to_string());
|
||||||
self.config.selected_mailbox = Some(mailbox);
|
self.config.selected_mailbox = Some(mailbox);
|
||||||
Ok(NewlySelected::Yes)
|
Ok(NewlySelected::Yes)
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ impl Imap {
|
|||||||
Err(Error::ConnectionLost)
|
Err(Error::ConnectionLost)
|
||||||
}
|
}
|
||||||
Err(async_imap::error::Error::Validate(_)) => {
|
Err(async_imap::error::Error::Validate(_)) => {
|
||||||
Err(Error::BadFolderName(folder.as_ref().to_string()))
|
Err(Error::BadFolderName(folder.to_string()))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.config.selected_folder = None;
|
self.config.selected_folder = None;
|
||||||
|
|||||||
150
src/imex.rs
150
src/imex.rs
@@ -3,17 +3,18 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
use ::pgp::types::KeyTrait;
|
||||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||||
use async_std::path::{Path, PathBuf};
|
|
||||||
use async_std::{
|
use async_std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
|
path::{Path, PathBuf},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
use async_tar::Archive;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::chat;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::delete_and_reset_all_device_msgs;
|
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
@@ -24,15 +25,13 @@ use crate::dc_tools::{
|
|||||||
use crate::e2ee;
|
use crate::e2ee;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||||
|
use crate::log::LogExt;
|
||||||
use crate::message::{Message, MsgId};
|
use crate::message::{Message, MsgId};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
use crate::pgp;
|
use crate::pgp;
|
||||||
use crate::sql::{self, Sql};
|
use crate::sql::{self, Sql};
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::{blob::BlobObject, log::LogExt};
|
|
||||||
use ::pgp::types::KeyTrait;
|
|
||||||
use async_tar::Archive;
|
|
||||||
|
|
||||||
// Name of the database file in the backup.
|
// Name of the database file in the backup.
|
||||||
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
|
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
|
||||||
@@ -79,7 +78,7 @@ pub enum ImexMode {
|
|||||||
///
|
///
|
||||||
/// Only one import-/export-progress can run at the same time.
|
/// Only one import-/export-progress can run at the same time.
|
||||||
/// To cancel an import-/export-progress, drop the future returned by this function.
|
/// To cancel an import-/export-progress, drop the future returned by this function.
|
||||||
pub async fn imex(context: &Context, what: ImexMode, param1: impl AsRef<Path>) -> Result<()> {
|
pub async fn imex(context: &Context, what: ImexMode, param1: &Path) -> Result<()> {
|
||||||
let cancel = context.alloc_ongoing().await?;
|
let cancel = context.alloc_ongoing().await?;
|
||||||
|
|
||||||
let res = async {
|
let res = async {
|
||||||
@@ -124,8 +123,7 @@ async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the filename of the backup found (otherwise an error)
|
/// 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 async fn has_backup(context: &Context, dir_name: &Path) -> Result<String> {
|
||||||
let dir_name = dir_name.as_ref();
|
|
||||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||||
let mut newest_backup_name = "".to_string();
|
let mut newest_backup_name = "".to_string();
|
||||||
let mut newest_backup_path: Option<PathBuf> = None;
|
let mut newest_backup_path: Option<PathBuf> = None;
|
||||||
@@ -155,8 +153,7 @@ pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the filename of the backup found (otherwise an error)
|
/// Returns the filename of the backup found (otherwise an error)
|
||||||
pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
|
pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String> {
|
||||||
let dir_name = dir_name.as_ref();
|
|
||||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||||
let mut newest_backup_time = 0;
|
let mut newest_backup_time = 0;
|
||||||
let mut newest_backup_name = "".to_string();
|
let mut newest_backup_name = "".to_string();
|
||||||
@@ -232,7 +229,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF).await?;
|
let chat_id = ChatId::create_for_contact(context, DC_CONTACT_ID_SELF).await?;
|
||||||
msg = Message::default();
|
msg = Message::default();
|
||||||
msg.viewtype = Viewtype::File;
|
msg.viewtype = Viewtype::File;
|
||||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||||
@@ -451,8 +448,8 @@ pub fn normalize_setup_code(s: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -> Result<()> {
|
async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()> {
|
||||||
info!(context, "Import/export dir: {}", path.as_ref().display());
|
info!(context, "Import/export dir: {}", path.display());
|
||||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||||
context.emit_event(EventType::ImexProgress(10));
|
context.emit_event(EventType::ImexProgress(10));
|
||||||
|
|
||||||
@@ -476,12 +473,8 @@ async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Import Backup
|
/// Import Backup
|
||||||
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> {
|
||||||
if backup_to_import
|
if backup_to_import.to_string_lossy().ends_with(".bak") {
|
||||||
.as_ref()
|
|
||||||
.to_string_lossy()
|
|
||||||
.ends_with(".bak")
|
|
||||||
{
|
|
||||||
// Backwards compability
|
// Backwards compability
|
||||||
return import_backup_old(context, backup_to_import).await;
|
return import_backup_old(context, backup_to_import).await;
|
||||||
}
|
}
|
||||||
@@ -489,7 +482,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
|||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Import \"{}\" to \"{}\".",
|
"Import \"{}\" to \"{}\".",
|
||||||
backup_to_import.as_ref().display(),
|
backup_to_import.display(),
|
||||||
context.get_dbfile().display()
|
context.get_dbfile().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -547,7 +540,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
|||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.open(context, &context.get_dbfile(), false)
|
.open(context, context.get_dbfile(), false)
|
||||||
.await
|
.await
|
||||||
.context("Could not re-open db")?;
|
.context("Could not re-open db")?;
|
||||||
|
|
||||||
@@ -556,11 +549,11 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result<()> {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Import \"{}\" to \"{}\".",
|
"Import \"{}\" to \"{}\".",
|
||||||
backup_to_import.as_ref().display(),
|
backup_to_import.display(),
|
||||||
context.get_dbfile().display()
|
context.get_dbfile().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -580,14 +573,14 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
|||||||
);
|
);
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()).await,
|
dc_copy_file(context, backup_to_import, context.get_dbfile()).await,
|
||||||
"could not copy file"
|
"could not copy file"
|
||||||
);
|
);
|
||||||
/* error already logged */
|
/* error already logged */
|
||||||
/* re-open copied database file */
|
/* re-open copied database file */
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.open(context, &context.get_dbfile(), false)
|
.open(context, context.get_dbfile(), false)
|
||||||
.await
|
.await
|
||||||
.context("Could not re-open db")?;
|
.context("Could not re-open db")?;
|
||||||
|
|
||||||
@@ -595,9 +588,8 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
|||||||
|
|
||||||
let total_files_cnt = context
|
let total_files_cnt = context
|
||||||
.sql
|
.sql
|
||||||
.count(sqlx::query("SELECT COUNT(*) FROM backup_blobs;"))
|
.count("SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||||
@@ -607,25 +599,33 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
|||||||
// consuming too much memory.
|
// consuming too much memory.
|
||||||
let file_ids = context
|
let file_ids = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(sqlx::query("SELECT id FROM backup_blobs ORDER BY id"))
|
.query_map(
|
||||||
.await?
|
"SELECT id FROM backup_blobs ORDER BY id",
|
||||||
.map(|row| row?.try_get(0))
|
paramsv![],
|
||||||
.collect::<sqlx::Result<Vec<i64>>>()
|
|row| row.get(0),
|
||||||
|
|ids| {
|
||||||
|
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut all_files_extracted = true;
|
let mut all_files_extracted = true;
|
||||||
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
||||||
// Load a single blob into memory
|
// Load a single blob into memory
|
||||||
let row = context
|
let (file_name, file_blob) = context
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(
|
.query_row(
|
||||||
sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
|
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||||
.bind(file_id),
|
paramsv![file_id],
|
||||||
|
|row| {
|
||||||
|
let file_name: String = row.get(0)?;
|
||||||
|
let file_blob: Vec<u8> = row.get(1)?;
|
||||||
|
Ok((file_name, file_blob))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let file_name: String = row.try_get(0)?;
|
|
||||||
let file_blob: &[u8] = row.try_get(1)?;
|
|
||||||
if context.shall_stop_ongoing().await {
|
if context.shall_stop_ongoing().await {
|
||||||
all_files_extracted = false;
|
all_files_extracted = false;
|
||||||
break;
|
break;
|
||||||
@@ -643,16 +643,16 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
|||||||
}
|
}
|
||||||
|
|
||||||
let path_filename = context.get_blobdir().join(file_name);
|
let path_filename = context.get_blobdir().join(file_name);
|
||||||
dc_write_file(context, &path_filename, file_blob).await?;
|
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if all_files_extracted {
|
if all_files_extracted {
|
||||||
// only delete backup_blobs if all files were successfully extracted
|
// only delete backup_blobs if all files were successfully extracted
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DROP TABLE backup_blobs;"))
|
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
context.sql.execute(sqlx::query("VACUUM;")).await.ok();
|
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
bail!("received stop signal");
|
bail!("received stop signal");
|
||||||
@@ -663,7 +663,7 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
|||||||
* Export backup
|
* Export backup
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
|
||||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||||
let now = time();
|
let now = time();
|
||||||
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
|
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
|
||||||
@@ -677,7 +677,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
|||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("VACUUM;"))
|
.execute("VACUUM;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
|
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
|
||||||
|
|
||||||
@@ -699,10 +699,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
|||||||
let res = export_backup_inner(context, &temp_path).await;
|
let res = export_backup_inner(context, &temp_path).await;
|
||||||
|
|
||||||
// we re-open the database after export is finished
|
// we re-open the database after export is finished
|
||||||
context
|
context.sql.open(context, context.get_dbfile(), false).await;
|
||||||
.sql
|
|
||||||
.open(context, &context.get_dbfile(), false)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match &res {
|
match &res {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -769,7 +766,7 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Classic key import
|
* Classic key import
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
|
||||||
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
|
/* 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.
|
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.
|
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
|
||||||
@@ -779,12 +776,12 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
|||||||
let mut set_default: bool;
|
let mut set_default: bool;
|
||||||
let mut imported_cnt = 0;
|
let mut imported_cnt = 0;
|
||||||
|
|
||||||
let dir_name = dir.as_ref().to_string_lossy();
|
let dir_name = dir.to_string_lossy();
|
||||||
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||||
while let Some(entry) = dir_handle.next().await {
|
while let Some(entry) = dir_handle.next().await {
|
||||||
let entry_fn = entry?.file_name();
|
let entry_fn = entry?.file_name();
|
||||||
let name_f = entry_fn.to_string_lossy();
|
let name_f = entry_fn.to_string_lossy();
|
||||||
let path_plus_name = dir.as_ref().join(&entry_fn);
|
let path_plus_name = dir.join(&entry_fn);
|
||||||
match dc_get_filesuffix_lc(&name_f) {
|
match dc_get_filesuffix_lc(&name_f) {
|
||||||
Some(suffix) => {
|
Some(suffix) => {
|
||||||
if suffix != "asc" {
|
if suffix != "asc" {
|
||||||
@@ -827,32 +824,35 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
|
||||||
let mut export_errors = 0;
|
let mut export_errors = 0;
|
||||||
|
|
||||||
let mut keys = context
|
let keys = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(sqlx::query(
|
.query_map(
|
||||||
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
||||||
))
|
paramsv![],
|
||||||
.await?
|
|row| {
|
||||||
.map(|row| -> sqlx::Result<_> {
|
let id = row.get(0)?;
|
||||||
let row = row?;
|
let public_key_blob: Vec<u8> = row.get(1)?;
|
||||||
let id = row.try_get(0)?;
|
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||||
let public_key_blob: &[u8] = row.try_get(1)?;
|
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||||
let public_key = SignedPublicKey::from_slice(public_key_blob);
|
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||||
let private_key_blob: &[u8] = row.try_get(2)?;
|
let is_default: i32 = row.get(3)?;
|
||||||
let private_key = SignedSecretKey::from_slice(private_key_blob);
|
|
||||||
let is_default: i32 = row.try_get(3)?;
|
|
||||||
|
|
||||||
Ok((id, public_key, private_key, is_default))
|
Ok((id, public_key, private_key, is_default))
|
||||||
});
|
},
|
||||||
|
|keys| {
|
||||||
|
keys.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
while let Some(parts) = keys.next().await {
|
for (id, public_key, private_key, is_default) in keys {
|
||||||
let (id, public_key, private_key, is_default) = parts?;
|
|
||||||
let id = Some(id).filter(|_| is_default != 0);
|
let id = Some(id).filter(|_| is_default != 0);
|
||||||
if let Ok(key) = public_key {
|
if let Ok(key) = public_key {
|
||||||
if export_key_to_asc_file(context, &dir, id, &key)
|
if export_key_to_asc_file(context, dir, id, &key)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@@ -862,7 +862,7 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
|||||||
export_errors += 1;
|
export_errors += 1;
|
||||||
}
|
}
|
||||||
if let Ok(key) = private_key {
|
if let Ok(key) = private_key {
|
||||||
if export_key_to_asc_file(context, &dir, id, &key)
|
if export_key_to_asc_file(context, dir, id, &key)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@@ -882,7 +882,7 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
async fn export_key_to_asc_file<T>(
|
async fn export_key_to_asc_file<T>(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
dir: impl AsRef<Path>,
|
dir: &Path,
|
||||||
id: Option<i64>,
|
id: Option<i64>,
|
||||||
key: &T,
|
key: &T,
|
||||||
) -> std::io::Result<()>
|
) -> std::io::Result<()>
|
||||||
@@ -899,7 +899,7 @@ where
|
|||||||
"unknown"
|
"unknown"
|
||||||
};
|
};
|
||||||
let id = id.map_or("default".into(), |i| i.to_string());
|
let id = id.map_or("default".into(), |i| i.to_string());
|
||||||
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
|
dir.join(format!("{}-key-{}.asc", kind, &id))
|
||||||
};
|
};
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
@@ -979,7 +979,7 @@ mod tests {
|
|||||||
async fn test_export_public_key_to_asc_file() {
|
async fn test_export_public_key_to_asc_file() {
|
||||||
let context = TestContext::new().await;
|
let context = TestContext::new().await;
|
||||||
let key = alice_keypair().public;
|
let key = alice_keypair().public;
|
||||||
let blobdir = "$BLOBDIR";
|
let blobdir = Path::new("$BLOBDIR");
|
||||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
||||||
.await
|
.await
|
||||||
.is_ok());
|
.is_ok());
|
||||||
@@ -994,7 +994,7 @@ mod tests {
|
|||||||
async fn test_export_private_key_to_asc_file() {
|
async fn test_export_private_key_to_asc_file() {
|
||||||
let context = TestContext::new().await;
|
let context = TestContext::new().await;
|
||||||
let key = alice_keypair().secret;
|
let key = alice_keypair().secret;
|
||||||
let blobdir = "$BLOBDIR";
|
let blobdir = Path::new("$BLOBDIR");
|
||||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
||||||
.await
|
.await
|
||||||
.is_ok());
|
.is_ok());
|
||||||
@@ -1009,7 +1009,7 @@ mod tests {
|
|||||||
async fn test_export_and_import_key() {
|
async fn test_export_and_import_key() {
|
||||||
let context = TestContext::new().await;
|
let context = TestContext::new().await;
|
||||||
context.configure_alice().await;
|
context.configure_alice().await;
|
||||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
let blobdir = context.ctx.get_blobdir();
|
||||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
|
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
|
||||||
panic!("got error on export: {:?}", err);
|
panic!("got error on export: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
268
src/job.rs
268
src/job.rs
@@ -7,37 +7,37 @@ use std::{fmt, time::Duration};
|
|||||||
|
|
||||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||||
use async_std::prelude::*;
|
|
||||||
use async_std::task::sleep;
|
use async_std::task::sleep;
|
||||||
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
|
use crate::blob::BlobObject;
|
||||||
|
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatItem};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_DEADDROP};
|
||||||
|
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||||
|
use crate::context::Context;
|
||||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||||
use crate::ephemeral::load_imap_deletion_msgid;
|
use crate::ephemeral::load_imap_deletion_msgid;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::imap::{Imap, ImapActionResult};
|
use crate::imap::{Imap, ImapActionResult};
|
||||||
use crate::location;
|
use crate::location;
|
||||||
use crate::message::MsgId;
|
use crate::log::LogExt;
|
||||||
use crate::message::{self, Message, MessageState};
|
use crate::message::{self, Message, MessageState, MsgId};
|
||||||
use crate::mimefactory::MimeFactory;
|
use crate::mimefactory::MimeFactory;
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
|
use crate::scheduler::InterruptInfo;
|
||||||
use crate::smtp::Smtp;
|
use crate::smtp::Smtp;
|
||||||
use crate::{blob::BlobObject, contact::normalize_name, contact::Modifier, contact::Origin};
|
use crate::sql;
|
||||||
use crate::{
|
|
||||||
chat::{self, Chat, ChatId, ChatItem},
|
|
||||||
constants::DC_CHAT_ID_DEADDROP,
|
|
||||||
};
|
|
||||||
use crate::{config::Config, constants::Blocked};
|
|
||||||
use crate::{constants::Chattype, contact::Contact};
|
|
||||||
use crate::{context::Context, log::LogExt};
|
|
||||||
use crate::{scheduler::InterruptInfo, sql};
|
|
||||||
|
|
||||||
// results in ~3 weeks for the last backoff timespan
|
// results in ~3 weeks for the last backoff timespan
|
||||||
const JOB_RETRIES: u32 = 17;
|
const JOB_RETRIES: u32 = 17;
|
||||||
|
|
||||||
/// Thread IDs
|
/// Thread IDs
|
||||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
#[derive(
|
||||||
|
Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||||
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub(crate) enum Thread {
|
pub(crate) enum Thread {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -75,7 +75,17 @@ impl Default for Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type,
|
Debug,
|
||||||
|
Display,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
FromPrimitive,
|
||||||
|
ToPrimitive,
|
||||||
|
FromSql,
|
||||||
|
ToSql,
|
||||||
)]
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
@@ -173,7 +183,7 @@ impl Job {
|
|||||||
if self.job_id != 0 {
|
if self.job_id != 0 {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32))
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,24 +202,26 @@ impl Job {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
paramsv![
|
||||||
)
|
self.desired_timestamp,
|
||||||
.bind(self.desired_timestamp)
|
self.tries as i64,
|
||||||
.bind(self.tries as i64)
|
self.param.to_string(),
|
||||||
.bind(self.param.to_string())
|
self.job_id as i32,
|
||||||
.bind(self.job_id as i32),
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
context.sql.execute(
|
context.sql.execute(
|
||||||
sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);")
|
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||||
.bind(self.added_timestamp)
|
paramsv![
|
||||||
.bind(thread)
|
self.added_timestamp,
|
||||||
.bind(self.action)
|
thread,
|
||||||
.bind(self.foreign_id)
|
self.action,
|
||||||
.bind(self.param.to_string())
|
self.foreign_id,
|
||||||
.bind(self.desired_timestamp)
|
self.param.to_string(),
|
||||||
|
self.desired_timestamp
|
||||||
|
]
|
||||||
).await?;
|
).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,32 +429,39 @@ impl Job {
|
|||||||
&self,
|
&self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
) -> sql::Result<(Vec<u32>, Vec<String>)> {
|
) -> Result<(Vec<u32>, Vec<String>)> {
|
||||||
// Extract message IDs from job parameters
|
// Extract message IDs from job parameters
|
||||||
let mut rows = context
|
let res: Vec<(u32, MsgId)> = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
|
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
|
||||||
.bind(contact_id)
|
paramsv![contact_id, self.job_id],
|
||||||
.bind(self.job_id),
|
|row| {
|
||||||
|
let job_id: u32 = row.get(0)?;
|
||||||
|
let params_str: String = row.get(1)?;
|
||||||
|
let params: Params = params_str.parse().unwrap_or_default();
|
||||||
|
Ok((job_id, params))
|
||||||
|
},
|
||||||
|
|jobs| {
|
||||||
|
let res = jobs
|
||||||
|
.filter_map(|row| {
|
||||||
|
let (job_id, params) = row.ok()?;
|
||||||
|
let msg_id = params.get_msg_id()?;
|
||||||
|
Some((job_id, msg_id))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(res)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Load corresponding RFC724 message IDs
|
// Load corresponding RFC724 message IDs
|
||||||
let mut job_ids = Vec::new();
|
let mut job_ids = Vec::new();
|
||||||
let mut rfc724_mids = Vec::new();
|
let mut rfc724_mids = Vec::new();
|
||||||
|
for (job_id, msg_id) in res {
|
||||||
while let Some(row) = rows.next().await {
|
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
|
||||||
let row = row?;
|
job_ids.push(job_id);
|
||||||
let job_id: u32 = row.try_get(0)?;
|
rfc724_mids.push(rfc724_mid);
|
||||||
let params_str: String = row.try_get(1)?;
|
|
||||||
let params: Params = params_str.parse().unwrap_or_default();
|
|
||||||
if let Some(msg_id) = params.get_msg_id() {
|
|
||||||
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await
|
|
||||||
{
|
|
||||||
job_ids.push(job_id);
|
|
||||||
rfc724_mids.push(rfc724_mid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((job_ids, rfc724_mids))
|
Ok((job_ids, rfc724_mids))
|
||||||
@@ -635,7 +654,6 @@ impl Job {
|
|||||||
// Hidden messages are similar to trashed, but are
|
// Hidden messages are similar to trashed, but are
|
||||||
// related to some chat. We also delete their
|
// related to some chat. We also delete their
|
||||||
// database records.
|
// database records.
|
||||||
info!(context, "verbose (issue 2335): will delete from db");
|
|
||||||
job_try!(msg.id.delete_from_db(context).await)
|
job_try!(msg.id.delete_from_db(context).await)
|
||||||
} else {
|
} else {
|
||||||
// Remove server UID from the database record.
|
// Remove server UID from the database record.
|
||||||
@@ -646,7 +664,6 @@ impl Job {
|
|||||||
// we remove UID to reduce the number of messages
|
// we remove UID to reduce the number of messages
|
||||||
// pointing to the corresponding UID. Once the counter
|
// pointing to the corresponding UID. Once the counter
|
||||||
// reaches zero, we will remove the message.
|
// reaches zero, we will remove the message.
|
||||||
info!(context, "verbose (issue 2335): will unlink");
|
|
||||||
job_try!(msg.id.unlink(context).await);
|
job_try!(msg.id.unlink(context).await);
|
||||||
}
|
}
|
||||||
Status::Finished(Ok(()))
|
Status::Finished(Ok(()))
|
||||||
@@ -711,10 +728,12 @@ impl Job {
|
|||||||
};
|
};
|
||||||
match chat.typ {
|
match chat.typ {
|
||||||
Chattype::Group | Chattype::Mailinglist => {
|
Chattype::Group | Chattype::Mailinglist => {
|
||||||
if let Ok((_1to1_chat, Blocked::Not)) =
|
if let Ok(Some(one_to_one_chat)) =
|
||||||
chat::lookup_by_contact_id(context, msg.from_id).await
|
ChatIdBlocked::lookup_by_contact(context, msg.from_id).await
|
||||||
{
|
{
|
||||||
chat.id.unblock(context).await;
|
if one_to_one_chat.blocked == Blocked::Not {
|
||||||
|
chat.id.unblock(context).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Chattype::Single | Chattype::Undefined => {}
|
Chattype::Single | Chattype::Undefined => {}
|
||||||
@@ -822,29 +841,31 @@ impl Job {
|
|||||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action))
|
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove jobs with specified IDs.
|
/// Remove jobs with specified IDs.
|
||||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||||
let q = format!(
|
let q = format!(
|
||||||
"DELETE FROM jobs WHERE id IN({})",
|
"DELETE FROM jobs WHERE id IN({})",
|
||||||
job_ids.iter().map(|_| "?").join(",")
|
job_ids.iter().map(|_| "?").join(",")
|
||||||
);
|
);
|
||||||
let mut query = sqlx::query(&q);
|
context
|
||||||
for id in job_ids {
|
.sql
|
||||||
query = query.bind(*id);
|
.execute(q, rusqlite::params_from_iter(job_ids))
|
||||||
}
|
.await?;
|
||||||
context.sql.execute(query).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action))
|
.exists(
|
||||||
|
"SELECT COUNT(*) FROM jobs WHERE action=?;",
|
||||||
|
paramsv![action],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -853,7 +874,7 @@ async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
|
|||||||
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
||||||
let chat_id: ChatId = context
|
let chat_id: ChatId = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id))
|
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![msg_id])
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
|
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
|
||||||
@@ -883,8 +904,8 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
|||||||
|
|
||||||
match Contact::add_or_lookup(
|
match Contact::add_or_lookup(
|
||||||
context,
|
context,
|
||||||
display_name_normalized,
|
&display_name_normalized,
|
||||||
contact.addr,
|
&contact.addr,
|
||||||
Origin::OutgoingTo,
|
Origin::OutgoingTo,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1030,9 +1051,8 @@ pub(crate) enum Connection<'a> {
|
|||||||
Smtp(&'a mut Smtp),
|
Smtp(&'a mut Smtp),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
|
||||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||||
info!(context, "verbose (issue 2335): loading imap deletion job");
|
|
||||||
Some(Job::new(
|
Some(Job::new(
|
||||||
Action::DeleteMsgOnImap,
|
Action::DeleteMsgOnImap,
|
||||||
msg_id.to_u32(),
|
msg_id.to_u32(),
|
||||||
@@ -1142,7 +1162,7 @@ async fn perform_job_action(
|
|||||||
) -> Status {
|
) -> Status {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"{} begin immediate try {} of job {:?} - verbose (issue 2335)", &connection, tries, job
|
"{} begin immediate try {} of job {}", &connection, tries, job
|
||||||
);
|
);
|
||||||
|
|
||||||
let try_res = match job.action {
|
let try_res = match job.action {
|
||||||
@@ -1285,77 +1305,65 @@ pub(crate) async fn load_next(
|
|||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let query;
|
||||||
|
let params;
|
||||||
let t = time();
|
let t = time();
|
||||||
|
let m;
|
||||||
let thread_i = thread as i64;
|
let thread_i = thread as i64;
|
||||||
|
|
||||||
let get_query = || {
|
if let Some(msg_id) = info.msg_id {
|
||||||
if let Some(msg_id) = info.msg_id {
|
query = r#"
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||||
FROM jobs
|
FROM jobs
|
||||||
WHERE thread=? AND foreign_id=?
|
WHERE thread=? AND foreign_id=?
|
||||||
ORDER BY action DESC, added_timestamp
|
ORDER BY action DESC, added_timestamp
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
"#,
|
"#;
|
||||||
)
|
m = msg_id;
|
||||||
.bind(thread_i)
|
params = paramsv![thread_i, m];
|
||||||
.bind(msg_id)
|
} else if !info.probe_network {
|
||||||
} else if !info.probe_network {
|
// processing for first-try and after backoff-timeouts:
|
||||||
// processing for first-try and after backoff-timeouts:
|
// process jobs in the order they were added.
|
||||||
// process jobs in the order they were added.
|
query = r#"
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||||
FROM jobs
|
FROM jobs
|
||||||
WHERE thread=? AND desired_timestamp<=?
|
WHERE thread=? AND desired_timestamp<=?
|
||||||
ORDER BY action DESC, added_timestamp
|
ORDER BY action DESC, added_timestamp
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
"#,
|
"#;
|
||||||
)
|
params = paramsv![thread_i, t];
|
||||||
.bind(thread_i)
|
} else {
|
||||||
.bind(t)
|
// processing after call to dc_maybe_network():
|
||||||
} else {
|
// process _all_ pending jobs that failed before
|
||||||
// processing after call to dc_maybe_network():
|
// in the order of their backoff-times.
|
||||||
// process _all_ pending jobs that failed before
|
query = r#"
|
||||||
// in the order of their backoff-times.
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||||
FROM jobs
|
FROM jobs
|
||||||
WHERE thread=? AND tries>0
|
WHERE thread=? AND tries>0
|
||||||
ORDER BY desired_timestamp, action DESC
|
ORDER BY desired_timestamp, action DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
"#,
|
"#;
|
||||||
)
|
params = paramsv![thread_i];
|
||||||
.bind(thread_i)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let job = loop {
|
let job = loop {
|
||||||
let job_res = context
|
let job_res = context
|
||||||
.sql
|
.sql
|
||||||
.fetch_optional(get_query())
|
.query_row_optional(query, params.clone(), |row| {
|
||||||
.await
|
let job = Job {
|
||||||
.and_then(|row| {
|
job_id: row.get("id")?,
|
||||||
if let Some(row) = row {
|
action: row.get("action")?,
|
||||||
Ok(Some(Job {
|
foreign_id: row.get("foreign_id")?,
|
||||||
job_id: row.try_get("id")?,
|
desired_timestamp: row.get("desired_timestamp")?,
|
||||||
action: row.try_get("action")?,
|
added_timestamp: row.get("added_timestamp")?,
|
||||||
foreign_id: row.try_get("foreign_id")?,
|
tries: row.get("tries")?,
|
||||||
desired_timestamp: row.try_get("desired_timestamp")?,
|
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||||
added_timestamp: row.try_get("added_timestamp")?,
|
pending_error: None,
|
||||||
tries: row.try_get::<i64, _>("tries")? as u32,
|
};
|
||||||
param: row
|
|
||||||
.try_get::<String, _>("param")?
|
Ok(job)
|
||||||
.parse()
|
})
|
||||||
.unwrap_or_default(),
|
.await;
|
||||||
pending_error: None,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match job_res {
|
match job_res {
|
||||||
Ok(job) => break job,
|
Ok(job) => break job,
|
||||||
@@ -1366,14 +1374,13 @@ LIMIT 1;
|
|||||||
// TODO: improve by only doing a single query
|
// TODO: improve by only doing a single query
|
||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(get_query())
|
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||||
.await
|
.await
|
||||||
.and_then(|row| row.try_get::<i32, _>(0).map_err(Into::into))
|
|
||||||
{
|
{
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
if let Err(err) = context
|
if let Err(err) = context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id))
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
warn!(context, "failed to delete job {}: {:?}", id, err);
|
warn!(context, "failed to delete job {}: {:?}", id, err);
|
||||||
@@ -1401,14 +1408,9 @@ LIMIT 1;
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.or(Some(job))
|
.or(Some(job))
|
||||||
} else {
|
} else {
|
||||||
info!(context, "verbose (issue 2335): executing job normally");
|
|
||||||
Some(job)
|
Some(job)
|
||||||
}
|
}
|
||||||
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
|
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
|
||||||
info!(
|
|
||||||
context,
|
|
||||||
"verbose (issue 2335): loaded imap deletion job (no others queued)"
|
|
||||||
);
|
|
||||||
Some(job)
|
Some(job)
|
||||||
} else {
|
} else {
|
||||||
load_housekeeping_job(context).await
|
load_housekeeping_job(context).await
|
||||||
@@ -1429,17 +1431,17 @@ mod tests {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO jobs
|
||||||
"INSERT INTO jobs
|
|
||||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);",
|
VALUES (?, ?, ?, ?, ?, ?);",
|
||||||
)
|
paramsv![
|
||||||
.bind(now)
|
now,
|
||||||
.bind(Thread::from(Action::MoveMsg))
|
Thread::from(Action::MoveMsg),
|
||||||
.bind(if valid { Action::MoveMsg as i32 } else { -1 })
|
if valid { Action::MoveMsg as i32 } else { -1 },
|
||||||
.bind(foreign_id)
|
foreign_id,
|
||||||
.bind(Params::new().to_string())
|
Params::new().to_string(),
|
||||||
.bind(now),
|
now
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1459,7 +1461,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||||
assert!(jobs.unwrap().action == Action::Housekeeping);
|
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
|
||||||
|
|
||||||
insert_job(&t, 1, true).await;
|
insert_job(&t, 1, true).await;
|
||||||
let jobs = load_next(
|
let jobs = load_next(
|
||||||
|
|||||||
66
src/key.rs
66
src/key.rs
@@ -9,14 +9,12 @@ use num_traits::FromPrimitive;
|
|||||||
use pgp::composed::Deserializable;
|
use pgp::composed::Deserializable;
|
||||||
use pgp::ser::Serialize;
|
use pgp::ser::Serialize;
|
||||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||||
use sqlx::Row;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::KeyGenType;
|
use crate::constants::KeyGenType;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
|
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
|
||||||
use crate::sql;
|
|
||||||
|
|
||||||
// Re-export key types
|
// Re-export key types
|
||||||
pub use crate::pgp::KeyPair;
|
pub use crate::pgp::KeyPair;
|
||||||
@@ -32,8 +30,6 @@ pub enum Error {
|
|||||||
Pgp(#[from] pgp::errors::Error),
|
Pgp(#[from] pgp::errors::Error),
|
||||||
#[error("Failed to generate PGP key: {}", _0)]
|
#[error("Failed to generate PGP key: {}", _0)]
|
||||||
Keygen(#[from] crate::pgp::PgpKeygenError),
|
Keygen(#[from] crate::pgp::PgpKeygenError),
|
||||||
#[error("Failed to load key: {}", _0)]
|
|
||||||
LoadKey(#[from] sql::Error),
|
|
||||||
#[error("Failed to save generated key: {}", _0)]
|
#[error("Failed to save generated key: {}", _0)]
|
||||||
StoreKey(#[from] SaveKeyError),
|
StoreKey(#[from] SaveKeyError),
|
||||||
#[error("No address configured")]
|
#[error("No address configured")]
|
||||||
@@ -42,8 +38,6 @@ pub enum Error {
|
|||||||
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||||
#[error("no data provided")]
|
#[error("no data provided")]
|
||||||
Empty,
|
Empty,
|
||||||
#[error("db: {}", _0)]
|
|
||||||
Sql(#[from] sqlx::Error),
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
@@ -123,17 +117,22 @@ impl DcKey for SignedPublicKey {
|
|||||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.fetch_optional(sqlx::query(
|
.query_row_optional(
|
||||||
r#"
|
r#"
|
||||||
SELECT public_key
|
SELECT public_key
|
||||||
FROM keypairs
|
FROM keypairs
|
||||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||||
AND is_default=1;
|
AND is_default=1;
|
||||||
"#,
|
"#,
|
||||||
))
|
paramsv![],
|
||||||
|
|row| {
|
||||||
|
let bytes: Vec<u8> = row.get(0)?;
|
||||||
|
Ok(bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(row) => Self::from_slice(row.try_get(0)?),
|
Some(bytes) => Self::from_slice(&bytes),
|
||||||
None => {
|
None => {
|
||||||
let keypair = generate_keypair(context).await?;
|
let keypair = generate_keypair(context).await?;
|
||||||
Ok(keypair.public)
|
Ok(keypair.public)
|
||||||
@@ -165,17 +164,22 @@ impl DcKey for SignedSecretKey {
|
|||||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.fetch_optional(sqlx::query(
|
.query_row_optional(
|
||||||
r#"
|
r#"
|
||||||
SELECT private_key
|
SELECT private_key
|
||||||
FROM keypairs
|
FROM keypairs
|
||||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||||
AND is_default=1;
|
AND is_default=1;
|
||||||
"#,
|
"#,
|
||||||
))
|
paramsv![],
|
||||||
|
|row| {
|
||||||
|
let bytes: Vec<u8> = row.get(0)?;
|
||||||
|
Ok(bytes)
|
||||||
|
},
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(row) => Self::from_slice(row.try_get(0)?),
|
Some(bytes) => Self::from_slice(&bytes),
|
||||||
None => {
|
None => {
|
||||||
let keypair = generate_keypair(context).await?;
|
let keypair = generate_keypair(context).await?;
|
||||||
Ok(keypair.secret)
|
Ok(keypair.secret)
|
||||||
@@ -228,23 +232,26 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
|||||||
// Check if the key appeared while we were waiting on the lock.
|
// Check if the key appeared while we were waiting on the lock.
|
||||||
match context
|
match context
|
||||||
.sql
|
.sql
|
||||||
.fetch_optional(
|
.query_row_optional(
|
||||||
sqlx::query(
|
r#"
|
||||||
r#"
|
|
||||||
SELECT public_key, private_key
|
SELECT public_key, private_key
|
||||||
FROM keypairs
|
FROM keypairs
|
||||||
WHERE addr=?1
|
WHERE addr=?1
|
||||||
AND is_default=1;
|
AND is_default=1;
|
||||||
"#,
|
"#,
|
||||||
)
|
paramsv![addr],
|
||||||
.bind(addr.to_string()),
|
|row| {
|
||||||
|
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||||
|
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||||
|
Ok((pub_bytes, sec_bytes))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(row) => Ok(KeyPair {
|
Some((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||||
addr,
|
addr,
|
||||||
public: SignedPublicKey::from_slice(row.try_get(0)?)?,
|
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||||
secret: SignedSecretKey::from_slice(row.try_get(1)?)?,
|
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||||
}),
|
}),
|
||||||
None => {
|
None => {
|
||||||
let start = std::time::SystemTime::now();
|
let start = std::time::SystemTime::now();
|
||||||
@@ -319,16 +326,15 @@ pub async fn store_self_keypair(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;")
|
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||||
.bind(&public_key)
|
paramsv![public_key, secret_key],
|
||||||
.bind(&secret_key),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||||
if default == KeyPairUse::Default {
|
if default == KeyPairUse::Default {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("UPDATE keypairs SET is_default=0;"))
|
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||||
}
|
}
|
||||||
@@ -343,15 +349,9 @@ pub async fn store_self_keypair(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
|
||||||
VALUES (?,?,?,?,?);",
|
VALUES (?,?,?,?,?);",
|
||||||
)
|
paramsv![addr, is_default, public_key, secret_key, t],
|
||||||
.bind(addr)
|
|
||||||
.bind(is_default)
|
|
||||||
.bind(&public_key)
|
|
||||||
.bind(&secret_key)
|
|
||||||
.bind(t),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
|
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
|
||||||
@@ -625,7 +625,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
|||||||
|
|
||||||
let nrows = || async {
|
let nrows = || async {
|
||||||
ctx.sql
|
ctx.sql
|
||||||
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
|
.count("SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@@ -1,11 +1,11 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
#![deny(
|
#![deny(
|
||||||
clippy::correctness,
|
clippy::correctness,
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
clippy::all,
|
clippy::all,
|
||||||
clippy::indexing_slicing,
|
clippy::indexing_slicing,
|
||||||
clippy::wildcard_imports,
|
clippy::wildcard_imports,
|
||||||
clippy::needless_borrow,
|
clippy::needless_borrow
|
||||||
unsafe_code
|
|
||||||
)]
|
)]
|
||||||
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
||||||
|
|
||||||
@@ -13,10 +13,16 @@
|
|||||||
extern crate num_derive;
|
extern crate num_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate smallvec;
|
extern crate smallvec;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rusqlite;
|
||||||
extern crate strum;
|
extern crate strum;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate strum_macros;
|
extern crate strum_macros;
|
||||||
|
|
||||||
|
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||||
|
|
||||||
|
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod log;
|
pub mod log;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
413
src/location.rs
413
src/location.rs
@@ -2,10 +2,8 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use anyhow::{ensure, Error};
|
use anyhow::{ensure, Error};
|
||||||
use async_std::prelude::*;
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::chat::{self, ChatId};
|
use crate::chat::{self, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -201,15 +199,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
|
|||||||
if context
|
if context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE chats \
|
||||||
"UPDATE chats \
|
|
||||||
SET locations_send_begin=?, \
|
SET locations_send_begin=?, \
|
||||||
locations_send_until=? \
|
locations_send_until=? \
|
||||||
WHERE id=?",
|
WHERE id=?",
|
||||||
)
|
paramsv![
|
||||||
.bind(if 0 != seconds { now } else { 0 })
|
if 0 != seconds { now } else { 0 },
|
||||||
.bind(if 0 != seconds { now + seconds } else { 0 })
|
if 0 != seconds { now + seconds } else { 0 },
|
||||||
.bind(chat_id),
|
chat_id,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -262,17 +260,16 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<Cha
|
|||||||
Some(chat_id) => context
|
Some(chat_id) => context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;")
|
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||||
.bind(chat_id)
|
paramsv![chat_id, time()],
|
||||||
.bind(time()),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
None => context
|
None => context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;")
|
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||||
.bind(time()),
|
paramsv![time()],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
@@ -285,29 +282,28 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
|||||||
}
|
}
|
||||||
let mut continue_streaming = false;
|
let mut continue_streaming = false;
|
||||||
|
|
||||||
if let Ok(mut chats) = context
|
if let Ok(chats) = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time()))
|
.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
|
.await
|
||||||
.map(|rows| rows.map(|row| row?.try_get::<i32, _>(0)))
|
|
||||||
{
|
{
|
||||||
while let Some(chat_id) = chats.next().await {
|
for chat_id in chats {
|
||||||
let chat_id = match chat_id {
|
|
||||||
Ok(id) => id,
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
if let Err(err) = context.sql.execute(
|
if let Err(err) = context.sql.execute(
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO locations \
|
"INSERT INTO locations \
|
||||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);"
|
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||||
)
|
paramsv![
|
||||||
.bind(latitude)
|
latitude,
|
||||||
.bind(longitude)
|
longitude,
|
||||||
.bind(accuracy)
|
accuracy,
|
||||||
.bind(time())
|
time(),
|
||||||
.bind(chat_id)
|
chat_id,
|
||||||
.bind(DC_CONTACT_ID_SELF)
|
DC_CONTACT_ID_SELF,
|
||||||
|
]
|
||||||
).await {
|
).await {
|
||||||
warn!(context, "failed to store location {:?}", err);
|
warn!(context, "failed to store location {:?}", err);
|
||||||
} else {
|
} else {
|
||||||
@@ -342,50 +338,54 @@ pub async fn get_range(
|
|||||||
Some(contact_id) => (0, contact_id),
|
Some(contact_id) => (0, contact_id),
|
||||||
None => (1, 0), // this contact_id is unused
|
None => (1, 0), // this contact_id is unused
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = context
|
let list = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
|
||||||
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
|
|
||||||
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
|
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
|
||||||
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
|
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
|
||||||
AND (? OR l.from_id=?) \
|
AND (? OR l.from_id=?) \
|
||||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
||||||
)
|
paramsv![
|
||||||
.bind(disable_chat_id)
|
disable_chat_id,
|
||||||
.bind(chat_id)
|
chat_id,
|
||||||
.bind(disable_contact_id)
|
disable_contact_id,
|
||||||
.bind(contact_id as i64)
|
contact_id as i32,
|
||||||
.bind(timestamp_from)
|
timestamp_from,
|
||||||
.bind(timestamp_to),
|
timestamp_to,
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
let msg_id = row.get(6)?;
|
||||||
|
let txt: String = row.get(9)?;
|
||||||
|
let marker = if msg_id != 0 && is_marker(&txt) {
|
||||||
|
Some(txt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let loc = Location {
|
||||||
|
location_id: row.get(0)?,
|
||||||
|
latitude: row.get(1)?,
|
||||||
|
longitude: row.get(2)?,
|
||||||
|
accuracy: row.get(3)?,
|
||||||
|
timestamp: row.get(4)?,
|
||||||
|
independent: row.get(5)?,
|
||||||
|
msg_id,
|
||||||
|
contact_id: row.get(7)?,
|
||||||
|
chat_id: row.get(8)?,
|
||||||
|
marker,
|
||||||
|
};
|
||||||
|
Ok(loc)
|
||||||
|
},
|
||||||
|
|locations| {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
for location in locations {
|
||||||
|
ret.push(location?);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await?
|
|
||||||
.map(|row| {
|
|
||||||
let row = row?;
|
|
||||||
let msg_id = row.try_get(6)?;
|
|
||||||
let txt: String = row.try_get(9)?;
|
|
||||||
let marker = if msg_id != 0 && is_marker(&txt) {
|
|
||||||
Some(txt)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let loc = Location {
|
|
||||||
location_id: row.try_get(0)?,
|
|
||||||
latitude: row.try_get(1)?,
|
|
||||||
longitude: row.try_get(2)?,
|
|
||||||
accuracy: row.try_get(3)?,
|
|
||||||
timestamp: row.try_get(4)?,
|
|
||||||
independent: row.try_get(5)?,
|
|
||||||
msg_id,
|
|
||||||
contact_id: row.try_get(7)?,
|
|
||||||
chat_id: row.try_get(8)?,
|
|
||||||
marker,
|
|
||||||
};
|
|
||||||
Ok(loc)
|
|
||||||
})
|
|
||||||
.collect::<sqlx::Result<_>>()
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
@@ -403,7 +403,7 @@ fn is_marker(txt: &str) -> bool {
|
|||||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM locations;"))
|
.execute("DELETE FROM locations;", paramsv![])
|
||||||
.await?;
|
.await?;
|
||||||
context.emit_event(EventType::LocationChanged(None));
|
context.emit_event(EventType::LocationChanged(None));
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -417,65 +417,70 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
|||||||
.await?
|
.await?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let (locations_send_begin, locations_send_until, locations_last_sent) = {
|
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||||
let row = context.sql.fetch_one(
|
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||||
sqlx::query(
|
paramsv![chat_id], |row| {
|
||||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;"
|
let send_begin: i64 = row.get(0)?;
|
||||||
)
|
let send_until: i64 = row.get(1)?;
|
||||||
.bind(chat_id)
|
let last_sent: i64 = row.get(2)?;
|
||||||
).await?;
|
|
||||||
|
|
||||||
let send_begin: i64 = row.try_get(0)?;
|
Ok((send_begin, send_until, last_sent))
|
||||||
let send_until: i64 = row.try_get(1)?;
|
})
|
||||||
let last_sent: i64 = row.try_get(2)?;
|
.await?;
|
||||||
|
|
||||||
(send_begin, send_until, last_sent)
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = time();
|
let now = time();
|
||||||
let mut location_count = 0;
|
let mut location_count = 0;
|
||||||
let mut ret = String::new();
|
let mut ret = String::new();
|
||||||
if locations_send_begin != 0 && now <= locations_send_until {
|
if locations_send_begin != 0 && now <= locations_send_until {
|
||||||
ret += &format!(
|
ret += &format!(
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||||
|
<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
|
||||||
self_addr,
|
self_addr,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut rows = context.sql.fetch(
|
context
|
||||||
sqlx::query(
|
.sql
|
||||||
|
.query_map(
|
||||||
"SELECT id, latitude, longitude, accuracy, timestamp \
|
"SELECT id, latitude, longitude, accuracy, timestamp \
|
||||||
FROM locations WHERE from_id=? \
|
FROM locations WHERE from_id=? \
|
||||||
AND timestamp>=? \
|
AND timestamp>=? \
|
||||||
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
AND (timestamp>=? OR \
|
||||||
|
timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
||||||
AND independent=0 \
|
AND independent=0 \
|
||||||
GROUP BY timestamp \
|
GROUP BY timestamp \
|
||||||
ORDER BY timestamp;"
|
ORDER BY timestamp;",
|
||||||
|
paramsv![
|
||||||
|
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)?;
|
||||||
|
let longitude: f64 = row.get(2)?;
|
||||||
|
let accuracy: f64 = row.get(3)?;
|
||||||
|
let timestamp = get_kml_timestamp(row.get(4)?);
|
||||||
|
|
||||||
|
Ok((location_id, latitude, longitude, accuracy, timestamp))
|
||||||
|
},
|
||||||
|
|rows| {
|
||||||
|
for row in rows {
|
||||||
|
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
|
||||||
|
ret += &format!(
|
||||||
|
"<Placemark>\
|
||||||
|
<Timestamp><when>{}</when></Timestamp>\
|
||||||
|
<Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point>\
|
||||||
|
</Placemark>\n",
|
||||||
|
timestamp, accuracy, longitude, latitude
|
||||||
|
);
|
||||||
|
location_count += 1;
|
||||||
|
last_added_location_id = location_id as u32;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.bind(DC_CONTACT_ID_SELF)
|
.await?;
|
||||||
.bind(locations_send_begin)
|
|
||||||
.bind(locations_last_sent)
|
|
||||||
.bind(DC_CONTACT_ID_SELF)
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
let location_id: u32 = row.try_get(0)?;
|
|
||||||
let latitude: f64 = row.try_get(1)?;
|
|
||||||
let longitude: f64 = row.try_get(2)?;
|
|
||||||
let accuracy: f64 = row.try_get(3)?;
|
|
||||||
let timestamp = get_kml_timestamp(row.try_get(4)?);
|
|
||||||
|
|
||||||
ret += &format!(
|
|
||||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
|
||||||
timestamp,
|
|
||||||
accuracy,
|
|
||||||
longitude,
|
|
||||||
latitude
|
|
||||||
);
|
|
||||||
location_count += 1;
|
|
||||||
last_added_location_id = location_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret += "</Document>\n</kml>";
|
ret += "</Document>\n</kml>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,9 +521,8 @@ pub async fn set_kml_sent_timestamp(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;")
|
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||||
.bind(timestamp)
|
paramsv![timestamp, chat_id],
|
||||||
.bind(chat_id),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -532,9 +536,8 @@ pub async fn set_msg_location_id(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;")
|
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||||
.bind(location_id)
|
paramsv![location_id, msg_id],
|
||||||
.bind(msg_id),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -553,7 +556,6 @@ pub async fn save(
|
|||||||
let mut newest_timestamp = 0;
|
let mut newest_timestamp = 0;
|
||||||
let mut newest_location_id = 0;
|
let mut newest_location_id = 0;
|
||||||
|
|
||||||
let stmt_test = "SELECT COUNT(*) FROM locations WHERE timestamp=? AND from_id=?";
|
|
||||||
let stmt_insert = "INSERT INTO locations\
|
let stmt_insert = "INSERT INTO locations\
|
||||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||||
VALUES (?,?,?,?,?,?,?);";
|
VALUES (?,?,?,?,?,?,?);";
|
||||||
@@ -566,28 +568,31 @@ pub async fn save(
|
|||||||
accuracy,
|
accuracy,
|
||||||
..
|
..
|
||||||
} = location;
|
} = location;
|
||||||
let exists = context
|
|
||||||
.sql
|
let conn = context.sql.get_conn().await?;
|
||||||
.exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id))
|
let mut stmt_test =
|
||||||
.await?;
|
conn.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||||
|
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
|
||||||
|
|
||||||
|
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||||
|
|
||||||
if independent || !exists {
|
if independent || !exists {
|
||||||
let row_id = context
|
stmt_insert.execute(paramsv![
|
||||||
.sql
|
timestamp,
|
||||||
.insert(
|
contact_id as i32,
|
||||||
sqlx::query(stmt_insert)
|
chat_id,
|
||||||
.bind(timestamp)
|
latitude,
|
||||||
.bind(contact_id)
|
longitude,
|
||||||
.bind(chat_id)
|
accuracy,
|
||||||
.bind(latitude)
|
independent,
|
||||||
.bind(longitude)
|
])?;
|
||||||
.bind(accuracy)
|
|
||||||
.bind(independent),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if timestamp > newest_timestamp {
|
if timestamp > newest_timestamp {
|
||||||
|
// okay to drop, as we use cached prepared statements
|
||||||
|
drop(stmt_test);
|
||||||
|
drop(stmt_insert);
|
||||||
newest_timestamp = timestamp;
|
newest_timestamp = timestamp;
|
||||||
newest_location_id = row_id;
|
newest_location_id = conn.last_insert_rowid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,21 +610,15 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
|||||||
|
|
||||||
let rows = context
|
let rows = context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT id, locations_send_begin, locations_last_sent \
|
||||||
"SELECT id, locations_send_begin, locations_last_sent \
|
|
||||||
FROM chats \
|
FROM chats \
|
||||||
WHERE locations_send_until>?;",
|
WHERE locations_send_until>?;",
|
||||||
)
|
paramsv![now],
|
||||||
.bind(now),
|
|row| {
|
||||||
)
|
let chat_id: ChatId = row.get(0)?;
|
||||||
.await
|
let locations_send_begin: i64 = row.get(1)?;
|
||||||
.map(|rows| {
|
let locations_last_sent: i64 = row.get(2)?;
|
||||||
rows.map(|row| -> sqlx::Result<Option<_>> {
|
|
||||||
let row = row?;
|
|
||||||
let chat_id: ChatId = row.try_get(0)?;
|
|
||||||
let locations_send_begin: i64 = row.try_get(1)?;
|
|
||||||
let locations_last_sent: i64 = row.try_get(2)?;
|
|
||||||
continue_streaming = true;
|
continue_streaming = true;
|
||||||
|
|
||||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||||
@@ -628,53 +627,57 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
|||||||
} else {
|
} else {
|
||||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.filter_map(|v| v.transpose())
|
|rows| {
|
||||||
});
|
rows.filter_map(|v| v.transpose())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let stmt = "SELECT COUNT(*) \
|
if let Ok(rows) = rows {
|
||||||
FROM locations \
|
|
||||||
WHERE from_id=? \
|
|
||||||
AND timestamp>=? \
|
|
||||||
AND timestamp>? \
|
|
||||||
AND independent=0 \
|
|
||||||
ORDER BY timestamp;";
|
|
||||||
|
|
||||||
if let Ok(mut rows) = rows {
|
|
||||||
let mut msgs = Vec::new();
|
let mut msgs = Vec::new();
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let (chat_id, locations_send_begin, locations_last_sent) = match row {
|
|
||||||
Ok(row) => row,
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
let exists = context
|
|
||||||
.sql
|
|
||||||
.exists(
|
|
||||||
sqlx::query(stmt)
|
|
||||||
.bind(DC_CONTACT_ID_SELF)
|
|
||||||
.bind(locations_send_begin)
|
|
||||||
.bind(locations_last_sent),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default(); // TODO: better error handling
|
|
||||||
|
|
||||||
if !exists {
|
{
|
||||||
// if there is no new location, there's nothing to send.
|
let conn = job_try!(context.sql.get_conn().await);
|
||||||
// however, maybe we want to bypass this test eg. 15 minutes
|
|
||||||
} else {
|
let mut stmt_locations = job_try!(conn.prepare_cached(
|
||||||
// pending locations are attached automatically to every message,
|
"SELECT id \
|
||||||
// so also to this empty text message.
|
FROM locations \
|
||||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
WHERE from_id=? \
|
||||||
//
|
AND timestamp>=? \
|
||||||
// for optimisation and to avoid flooding the sending queue,
|
AND timestamp>? \
|
||||||
// we could sending these messages only if we're really online.
|
AND independent=0 \
|
||||||
// the easiest way to determine this, is to check for an empty message queue.
|
ORDER BY timestamp;",
|
||||||
// (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);
|
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
|
||||||
msg.hidden = true;
|
if !stmt_locations
|
||||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
.exists(paramsv![
|
||||||
msgs.push((chat_id, msg));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,16 +705,16 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
|||||||
|
|
||||||
let chat_id = ChatId::new(job.foreign_id);
|
let chat_id = ChatId::new(job.foreign_id);
|
||||||
|
|
||||||
let (send_begin, send_until) = job_try!(context
|
let (send_begin, send_until) = job_try!(
|
||||||
.sql
|
context
|
||||||
.fetch_one(
|
.sql
|
||||||
sqlx::query(
|
.query_row(
|
||||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||||
|
paramsv![chat_id],
|
||||||
|
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||||
)
|
)
|
||||||
.bind(chat_id)
|
.await
|
||||||
)
|
);
|
||||||
.await
|
|
||||||
.and_then(|row| { Ok((row.try_get::<i64, _>(0)?, row.try_get::<i64, _>(1)?)) }));
|
|
||||||
|
|
||||||
if !(send_begin != 0 && time() <= send_until) {
|
if !(send_begin != 0 && time() <= send_until) {
|
||||||
// still streaming -
|
// still streaming -
|
||||||
@@ -723,12 +726,10 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"UPDATE chats \
|
||||||
"UPDATE chats \
|
|
||||||
SET locations_send_begin=0, locations_send_until=0 \
|
SET locations_send_begin=0, locations_send_until=0 \
|
||||||
WHERE id=?"
|
WHERE id=?",
|
||||||
)
|
paramsv![chat_id],
|
||||||
.bind(chat_id)
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Default, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn log_err_inner(self, context: &Context, msg: Option<&str>) -> Result<T, E> {
|
fn log_err_inner(self, context: &Context, msg: Option<&str>) -> Result<T, E> {
|
||||||
if let Err(e) = &self {
|
if let Err(e) = &self {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::fmt;
|
|||||||
|
|
||||||
use crate::provider::{get_provider_by_id, Provider};
|
use crate::provider::{get_provider_by_id, Provider};
|
||||||
use crate::{context::Context, provider::Socket};
|
use crate::{context::Context, provider::Socket};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
@@ -54,10 +55,7 @@ pub struct LoginParam {
|
|||||||
|
|
||||||
impl LoginParam {
|
impl LoginParam {
|
||||||
/// Read the login parameters from the database.
|
/// Read the login parameters from the database.
|
||||||
pub async fn from_database(
|
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
|
||||||
context: &Context,
|
|
||||||
prefix: impl AsRef<str>,
|
|
||||||
) -> crate::sql::Result<Self> {
|
|
||||||
let prefix = prefix.as_ref();
|
let prefix = prefix.as_ref();
|
||||||
let sql = &context.sql;
|
let sql = &context.sql;
|
||||||
|
|
||||||
@@ -156,11 +154,7 @@ impl LoginParam {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save this loginparam to the database.
|
/// Save this loginparam to the database.
|
||||||
pub async fn save_to_database(
|
pub async fn save_to_database(&self, context: &Context, prefix: impl AsRef<str>) -> Result<()> {
|
||||||
&self,
|
|
||||||
context: &Context,
|
|
||||||
prefix: impl AsRef<str>,
|
|
||||||
) -> crate::sql::Result<()> {
|
|
||||||
let prefix = prefix.as_ref();
|
let prefix = prefix.as_ref();
|
||||||
let sql = &context.sql;
|
let sql = &context.sql;
|
||||||
|
|
||||||
@@ -317,7 +311,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn test_save_load_login_param() -> anyhow::Result<()> {
|
async fn test_save_load_login_param() -> Result<()> {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
let param = LoginParam {
|
let param = LoginParam {
|
||||||
|
|||||||
10
src/lot.rs
10
src/lot.rs
@@ -1,3 +1,5 @@
|
|||||||
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
|
|
||||||
use crate::key::Fingerprint;
|
use crate::key::Fingerprint;
|
||||||
|
|
||||||
/// An object containing a set of values.
|
/// An object containing a set of values.
|
||||||
@@ -20,7 +22,9 @@ pub struct Lot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||||
|
)]
|
||||||
pub enum Meaning {
|
pub enum Meaning {
|
||||||
None = 0,
|
None = 0,
|
||||||
Text1Draft = 1,
|
Text1Draft = 1,
|
||||||
@@ -64,8 +68,10 @@ impl Lot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||||
|
)]
|
||||||
pub enum LotState {
|
pub enum LotState {
|
||||||
// Default
|
// Default
|
||||||
Undefined = 0,
|
Undefined = 0,
|
||||||
|
|||||||
844
src/message.rs
844
src/message.rs
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,8 @@
|
|||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, format_err, Result};
|
use anyhow::{bail, ensure, format_err, Result};
|
||||||
use async_std::prelude::*;
|
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||||
use sqlx::Row;
|
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::{self, Chat};
|
use crate::chat::{self, Chat};
|
||||||
@@ -85,6 +83,33 @@ pub struct RenderedEmail {
|
|||||||
pub subject: String,
|
pub subject: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct MessageHeaders {
|
||||||
|
/// Opportunistically protected headers.
|
||||||
|
///
|
||||||
|
/// These headers are placed into encrypted part *if* the message is encrypted. Place headers
|
||||||
|
/// which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the
|
||||||
|
/// message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here.
|
||||||
|
///
|
||||||
|
/// If the message is not encrypted, these headers are placed into IMF header section, so make
|
||||||
|
/// sure that the message will be encrypted if you place any sensitive information here.
|
||||||
|
pub protected: Vec<Header>,
|
||||||
|
|
||||||
|
/// Headers that must go into IMF header section.
|
||||||
|
///
|
||||||
|
/// These are standard headers such as Date, In-Reply-To, References, which cannot be placed
|
||||||
|
/// anywhere else according to the standard. Placing headers here also allows them to be fetched
|
||||||
|
/// individually over IMAP without downloading the message body. This is why Chat-Version is
|
||||||
|
/// placed here.
|
||||||
|
pub unprotected: Vec<Header>,
|
||||||
|
|
||||||
|
/// Headers that MUST NOT go into IMF header section.
|
||||||
|
///
|
||||||
|
/// These are large headers which may hit the header section size limit on the server, such as
|
||||||
|
/// Chat-User-Avatar with a base64-encoded image inside.
|
||||||
|
pub hidden: Vec<Header>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> MimeFactory<'a> {
|
impl<'a> MimeFactory<'a> {
|
||||||
pub async fn from_msg(
|
pub async fn from_msg(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
@@ -115,42 +140,51 @@ impl<'a> MimeFactory<'a> {
|
|||||||
if chat.is_self_talk() {
|
if chat.is_self_talk() {
|
||||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||||
} else {
|
} else {
|
||||||
let mut rows = context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch(
|
.query_map(
|
||||||
sqlx::query(
|
"SELECT c.authname, c.addr \
|
||||||
"SELECT c.authname, c.addr \
|
|
||||||
FROM chats_contacts cc \
|
FROM chats_contacts cc \
|
||||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||||
)
|
paramsv![msg.chat_id],
|
||||||
.bind(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?;
|
.await?;
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
let authname: String = row.try_get(0)?;
|
|
||||||
let addr: String = row.try_get(1)?;
|
|
||||||
if !recipients_contain_addr(&recipients, &addr) {
|
|
||||||
recipients.push((authname, addr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
|
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
|
||||||
req_mdn = true;
|
req_mdn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let row = context
|
let (in_reply_to, references) = context
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(
|
.query_row(
|
||||||
sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?")
|
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||||
.bind(msg.id),
|
paramsv![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?;
|
.await?;
|
||||||
let (in_reply_to, references) = (
|
|
||||||
render_rfc724_mid_list(row.try_get(0)?),
|
|
||||||
render_rfc724_mid_list(row.try_get(1)?),
|
|
||||||
);
|
|
||||||
|
|
||||||
let default_str = stock_str::status_line(context).await;
|
let default_str = stock_str::status_line(context).await;
|
||||||
let factory = MimeFactory {
|
let factory = MimeFactory {
|
||||||
@@ -402,14 +436,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
|
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
|
||||||
// Headers that are encrypted
|
let mut headers: MessageHeaders = Default::default();
|
||||||
// - Chat-*, except Chat-Version
|
|
||||||
// - Secure-Join*
|
|
||||||
// - Subject
|
|
||||||
let mut protected_headers: Vec<Header> = Vec::new();
|
|
||||||
|
|
||||||
// All other headers
|
|
||||||
let mut unprotected_headers: Vec<Header> = Vec::new();
|
|
||||||
|
|
||||||
let from = Address::new_mailbox_with_name(
|
let from = Address::new_mailbox_with_name(
|
||||||
self.from_displayname.to_string(),
|
self.from_displayname.to_string(),
|
||||||
@@ -432,14 +459,20 @@ impl<'a> MimeFactory<'a> {
|
|||||||
to.push(from.clone());
|
to.push(from.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into()));
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||||
|
|
||||||
if !self.references.is_empty() {
|
if !self.references.is_empty() {
|
||||||
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new("References".into(), self.references.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.in_reply_to.is_empty() {
|
if !self.in_reply_to.is_empty() {
|
||||||
unprotected_headers.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let date = chrono::Utc
|
let date = chrono::Utc
|
||||||
@@ -447,12 +480,14 @@ impl<'a> MimeFactory<'a> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_rfc2822();
|
.to_rfc2822();
|
||||||
|
|
||||||
unprotected_headers.push(Header::new("Date".into(), date));
|
headers.unprotected.push(Header::new("Date".into(), date));
|
||||||
|
|
||||||
unprotected_headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||||
|
|
||||||
if let Loaded::Mdn { .. } = self.loaded {
|
if let Loaded::Mdn { .. } = self.loaded {
|
||||||
unprotected_headers.push(Header::new(
|
headers.unprotected.push(Header::new(
|
||||||
"Auto-Submitted".to_string(),
|
"Auto-Submitted".to_string(),
|
||||||
"auto-replied".to_string(),
|
"auto-replied".to_string(),
|
||||||
));
|
));
|
||||||
@@ -462,7 +497,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
// we use "Chat-Disposition-Notification-To"
|
// we use "Chat-Disposition-Notification-To"
|
||||||
// because replies to "Disposition-Notification-To" are weird in many cases
|
// because replies to "Disposition-Notification-To" are weird in many cases
|
||||||
// eg. are just freetext and/or do not follow any standard.
|
// eg. are just freetext and/or do not follow any standard.
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Disposition-Notification-To".into(),
|
"Chat-Disposition-Notification-To".into(),
|
||||||
self.from_addr.clone(),
|
self.from_addr.clone(),
|
||||||
));
|
));
|
||||||
@@ -490,10 +525,14 @@ impl<'a> MimeFactory<'a> {
|
|||||||
if !skip_autocrypt {
|
if !skip_autocrypt {
|
||||||
// unless determined otherwise we add the Autocrypt header
|
// unless determined otherwise we add the Autocrypt header
|
||||||
let aheader = encrypt_helper.get_aheader().to_string();
|
let aheader = encrypt_helper.get_aheader().to_string();
|
||||||
unprotected_headers.push(Header::new("Autocrypt".into(), aheader));
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new("Autocrypt".into(), aheader));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected_headers.push(Header::new("Subject".into(), encoded_subject));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Subject".into(), encoded_subject));
|
||||||
|
|
||||||
let rfc724_mid = match self.loaded {
|
let rfc724_mid = match self.loaded {
|
||||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||||
@@ -502,23 +541,28 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|
|
||||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
|
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
|
||||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Ephemeral-Timer".to_string(),
|
"Ephemeral-Timer".to_string(),
|
||||||
duration.to_string(),
|
duration.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
unprotected_headers.push(Header::new(
|
headers.unprotected.push(Header::new(
|
||||||
"Message-ID".into(),
|
"Message-ID".into(),
|
||||||
render_rfc724_mid(&rfc724_mid),
|
render_rfc724_mid(&rfc724_mid),
|
||||||
));
|
));
|
||||||
|
|
||||||
unprotected_headers.push(Header::new_with_value("To".into(), to).unwrap());
|
headers
|
||||||
unprotected_headers.push(Header::new_with_value("From".into(), vec![from]).unwrap());
|
.unprotected
|
||||||
|
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||||
|
headers
|
||||||
|
.unprotected
|
||||||
|
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
|
||||||
if let Some(sender_displayname) = &self.sender_displayname {
|
if let Some(sender_displayname) = &self.sender_displayname {
|
||||||
let sender =
|
let sender =
|
||||||
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
||||||
unprotected_headers
|
headers
|
||||||
|
.unprotected
|
||||||
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,13 +570,8 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|
|
||||||
let (main_part, parts) = match self.loaded {
|
let (main_part, parts) = match self.loaded {
|
||||||
Loaded::Message { .. } => {
|
Loaded::Message { .. } => {
|
||||||
self.render_message(
|
self.render_message(context, &mut headers, &grpimage)
|
||||||
context,
|
.await?
|
||||||
&mut protected_headers,
|
|
||||||
&mut unprotected_headers,
|
|
||||||
&grpimage,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()),
|
Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()),
|
||||||
};
|
};
|
||||||
@@ -555,12 +594,19 @@ impl<'a> MimeFactory<'a> {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store protected headers in the inner message.
|
|
||||||
let mut message = protected_headers
|
|
||||||
.into_iter()
|
|
||||||
.fold(message, |message, header| message.header(header));
|
|
||||||
|
|
||||||
let outer_message = if is_encrypted {
|
let outer_message = if is_encrypted {
|
||||||
|
// Store protected headers in the inner message.
|
||||||
|
let message = headers
|
||||||
|
.protected
|
||||||
|
.into_iter()
|
||||||
|
.fold(message, |message, header| message.header(header));
|
||||||
|
|
||||||
|
// Add hidden headers to encrypted payload.
|
||||||
|
let mut message = headers
|
||||||
|
.hidden
|
||||||
|
.into_iter()
|
||||||
|
.fold(message, |message, header| message.header(header));
|
||||||
|
|
||||||
// Add gossip headers in chats with multiple recipients
|
// Add gossip headers in chats with multiple recipients
|
||||||
if peerstates.len() > 1 && self.should_do_gossip(context).await? {
|
if peerstates.len() > 1 && self.should_do_gossip(context).await? {
|
||||||
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
||||||
@@ -594,11 +640,6 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(),
|
"multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Store the unprotected headers on the outer message.
|
|
||||||
let outer_message = unprotected_headers
|
|
||||||
.into_iter()
|
|
||||||
.fold(outer_message, |message, header| message.header(header));
|
|
||||||
|
|
||||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||||
info!(context, "mimefactory: outgoing message mime:");
|
info!(context, "mimefactory: outgoing message mime:");
|
||||||
let raw_message = message.clone().build().as_string();
|
let raw_message = message.clone().build().as_string();
|
||||||
@@ -633,11 +674,33 @@ impl<'a> MimeFactory<'a> {
|
|||||||
)
|
)
|
||||||
.header(("Subject".to_string(), "...".to_string()))
|
.header(("Subject".to_string(), "...".to_string()))
|
||||||
} else {
|
} else {
|
||||||
unprotected_headers
|
let message = if headers.hidden.is_empty() {
|
||||||
|
message
|
||||||
|
} else {
|
||||||
|
// Store hidden headers in the inner unencrypted message.
|
||||||
|
let message = headers
|
||||||
|
.hidden
|
||||||
|
.into_iter()
|
||||||
|
.fold(message, |message, header| message.header(header));
|
||||||
|
|
||||||
|
PartBuilder::new()
|
||||||
|
.message_type(MimeMultipartType::Mixed)
|
||||||
|
.child(message.build())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store protected headers in the outer message.
|
||||||
|
headers
|
||||||
|
.protected
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.fold(message, |message, header| message.header(header))
|
.fold(message, |message, header| message.header(header))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store the unprotected headers on the outer message.
|
||||||
|
let outer_message = headers
|
||||||
|
.unprotected
|
||||||
|
.into_iter()
|
||||||
|
.fold(outer_message, |message, header| message.header(header));
|
||||||
|
|
||||||
let MimeFactory {
|
let MimeFactory {
|
||||||
last_added_location_id,
|
last_added_location_id,
|
||||||
..
|
..
|
||||||
@@ -698,8 +761,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
async fn render_message(
|
async fn render_message(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
protected_headers: &mut Vec<Header>,
|
headers: &mut MessageHeaders,
|
||||||
unprotected_headers: &mut Vec<Header>,
|
|
||||||
grpimage: &Option<String>,
|
grpimage: &Option<String>,
|
||||||
) -> Result<(PartBuilder, Vec<PartBuilder>)> {
|
) -> Result<(PartBuilder, Vec<PartBuilder>)> {
|
||||||
let chat = match &self.loaded {
|
let chat = match &self.loaded {
|
||||||
@@ -711,20 +773,26 @@ impl<'a> MimeFactory<'a> {
|
|||||||
let mut meta_part = None;
|
let mut meta_part = None;
|
||||||
|
|
||||||
if chat.is_protected() {
|
if chat.is_protected() {
|
||||||
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if chat.typ == Chattype::Group {
|
if chat.typ == Chattype::Group {
|
||||||
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||||
|
|
||||||
let encoded = encode_words(&chat.name);
|
let encoded = encode_words(&chat.name);
|
||||||
protected_headers.push(Header::new("Chat-Group-Name".into(), encoded));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Group-Name".into(), encoded));
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
SystemMessage::MemberRemovedFromGroup => {
|
SystemMessage::MemberRemovedFromGroup => {
|
||||||
let email_to_remove = self.msg.param.get(Param::Arg).unwrap_or_default();
|
let email_to_remove = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||||
if !email_to_remove.is_empty() {
|
if !email_to_remove.is_empty() {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Group-Member-Removed".into(),
|
"Chat-Group-Member-Removed".into(),
|
||||||
email_to_remove.into(),
|
email_to_remove.into(),
|
||||||
));
|
));
|
||||||
@@ -733,7 +801,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
SystemMessage::MemberAddedToGroup => {
|
SystemMessage::MemberAddedToGroup => {
|
||||||
let email_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
|
let email_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||||
if !email_to_add.is_empty() {
|
if !email_to_add.is_empty() {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Group-Member-Added".into(),
|
"Chat-Group-Member-Added".into(),
|
||||||
email_to_add.into(),
|
email_to_add.into(),
|
||||||
));
|
));
|
||||||
@@ -746,7 +814,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>",
|
"sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>",
|
||||||
"vg-member-added",
|
"vg-member-added",
|
||||||
);
|
);
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Secure-Join".to_string(),
|
"Secure-Join".to_string(),
|
||||||
"vg-member-added".to_string(),
|
"vg-member-added".to_string(),
|
||||||
));
|
));
|
||||||
@@ -754,18 +822,18 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
SystemMessage::GroupNameChanged => {
|
SystemMessage::GroupNameChanged => {
|
||||||
let old_name = self.msg.param.get(Param::Arg).unwrap_or_default();
|
let old_name = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Group-Name-Changed".into(),
|
"Chat-Group-Name-Changed".into(),
|
||||||
maybe_encode_words(old_name),
|
maybe_encode_words(old_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::GroupImageChanged => {
|
SystemMessage::GroupImageChanged => {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".to_string(),
|
"Chat-Content".to_string(),
|
||||||
"group-avatar-changed".to_string(),
|
"group-avatar-changed".to_string(),
|
||||||
));
|
));
|
||||||
if grpimage.is_none() {
|
if grpimage.is_none() {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Group-Avatar".to_string(),
|
"Chat-Group-Avatar".to_string(),
|
||||||
"0".to_string(),
|
"0".to_string(),
|
||||||
));
|
));
|
||||||
@@ -777,13 +845,13 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|
|
||||||
match command {
|
match command {
|
||||||
SystemMessage::LocationStreamingEnabled => {
|
SystemMessage::LocationStreamingEnabled => {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".into(),
|
"Chat-Content".into(),
|
||||||
"location-streaming-enabled".into(),
|
"location-streaming-enabled".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::EphemeralTimerChanged => {
|
SystemMessage::EphemeralTimerChanged => {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".to_string(),
|
"Chat-Content".to_string(),
|
||||||
"ephemeral-timer-changed".to_string(),
|
"ephemeral-timer-changed".to_string(),
|
||||||
));
|
));
|
||||||
@@ -797,13 +865,14 @@ impl<'a> MimeFactory<'a> {
|
|||||||
// Adding this header without encryption leaks some
|
// Adding this header without encryption leaks some
|
||||||
// information about the message contents, but it can
|
// information about the message contents, but it can
|
||||||
// already be easily guessed from message timing and size.
|
// already be easily guessed from message timing and size.
|
||||||
unprotected_headers.push(Header::new(
|
headers.unprotected.push(Header::new(
|
||||||
"Auto-Submitted".to_string(),
|
"Auto-Submitted".to_string(),
|
||||||
"auto-generated".to_string(),
|
"auto-generated".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::AutocryptSetupMessage => {
|
SystemMessage::AutocryptSetupMessage => {
|
||||||
unprotected_headers
|
headers
|
||||||
|
.unprotected
|
||||||
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||||
|
|
||||||
placeholdertext = Some(stock_str::ac_setup_msg_body(context).await);
|
placeholdertext = Some(stock_str::ac_setup_msg_body(context).await);
|
||||||
@@ -816,11 +885,13 @@ impl<'a> MimeFactory<'a> {
|
|||||||
context,
|
context,
|
||||||
"sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>", step,
|
"sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>", step,
|
||||||
);
|
);
|
||||||
protected_headers.push(Header::new("Secure-Join".into(), step.into()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Secure-Join".into(), step.into()));
|
||||||
|
|
||||||
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
||||||
if !param2.is_empty() {
|
if !param2.is_empty() {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
||||||
"Secure-Join-Auth".into()
|
"Secure-Join-Auth".into()
|
||||||
} else {
|
} else {
|
||||||
@@ -832,24 +903,26 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|
|
||||||
let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default();
|
let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default();
|
||||||
if !fingerprint.is_empty() {
|
if !fingerprint.is_empty() {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Secure-Join-Fingerprint".into(),
|
"Secure-Join-Fingerprint".into(),
|
||||||
fingerprint.into(),
|
fingerprint.into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(id) = msg.param.get(Param::Arg4) {
|
if let Some(id) = msg.param.get(Param::Arg4) {
|
||||||
protected_headers.push(Header::new("Secure-Join-Group".into(), id.into()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Secure-Join-Group".into(), id.into()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SystemMessage::ChatProtectionEnabled => {
|
SystemMessage::ChatProtectionEnabled => {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".to_string(),
|
"Chat-Content".to_string(),
|
||||||
"protection-enabled".to_string(),
|
"protection-enabled".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::ChatProtectionDisabled => {
|
SystemMessage::ChatProtectionDisabled => {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".to_string(),
|
"Chat-Content".to_string(),
|
||||||
"protection-disabled".to_string(),
|
"protection-disabled".to_string(),
|
||||||
));
|
));
|
||||||
@@ -867,17 +940,21 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|
|
||||||
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").await?;
|
||||||
meta_part = Some(mail);
|
meta_part = Some(mail);
|
||||||
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.msg.viewtype == Viewtype::Sticker {
|
if self.msg.viewtype == Viewtype::Sticker {
|
||||||
protected_headers.push(Header::new("Chat-Content".into(), "sticker".into()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Content".into(), "sticker".into()));
|
||||||
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
|
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Content".into(),
|
"Chat-Content".into(),
|
||||||
"videochat-invitation".into(),
|
"videochat-invitation".into(),
|
||||||
));
|
));
|
||||||
protected_headers.push(Header::new(
|
headers.protected.push(Header::new(
|
||||||
"Chat-Webrtc-Room".into(),
|
"Chat-Webrtc-Room".into(),
|
||||||
self.msg
|
self.msg
|
||||||
.param
|
.param
|
||||||
@@ -892,12 +969,16 @@ impl<'a> MimeFactory<'a> {
|
|||||||
|| self.msg.viewtype == Viewtype::Video
|
|| self.msg.viewtype == Viewtype::Video
|
||||||
{
|
{
|
||||||
if self.msg.viewtype == Viewtype::Voice {
|
if self.msg.viewtype == Viewtype::Voice {
|
||||||
protected_headers.push(Header::new("Chat-Voice-Message".into(), "1".into()));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Voice-Message".into(), "1".into()));
|
||||||
}
|
}
|
||||||
let duration_ms = self.msg.param.get_int(Param::Duration).unwrap_or_default();
|
let duration_ms = self.msg.param.get_int(Param::Duration).unwrap_or_default();
|
||||||
if duration_ms > 0 {
|
if duration_ms > 0 {
|
||||||
let dur = duration_ms.to_string();
|
let dur = duration_ms.to_string();
|
||||||
protected_headers.push(Header::new("Chat-Duration".into(), dur));
|
headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-Duration".into(), dur));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,13 +1089,15 @@ impl<'a> MimeFactory<'a> {
|
|||||||
if self.attach_selfavatar {
|
if self.attach_selfavatar {
|
||||||
match context.get_config(Config::Selfavatar).await? {
|
match context.get_config(Config::Selfavatar).await? {
|
||||||
Some(path) => match build_selfavatar_file(context, &path) {
|
Some(path) => match build_selfavatar_file(context, &path) {
|
||||||
Ok((part, filename)) => {
|
Ok(avatar) => headers.hidden.push(Header::new(
|
||||||
parts.push(part);
|
"Chat-User-Avatar".into(),
|
||||||
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
|
format!("base64:{}", avatar),
|
||||||
}
|
)),
|
||||||
Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
|
Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
|
||||||
},
|
},
|
||||||
None => protected_headers.push(Header::new("Chat-User-Avatar".into(), "0".into())),
|
None => headers
|
||||||
|
.protected
|
||||||
|
.push(Header::new("Chat-User-Avatar".into(), "0".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,10 +1143,13 @@ impl<'a> MimeFactory<'a> {
|
|||||||
self.msg.get_summarytext(context, 32).await
|
self.msg.get_summarytext(context, 32).await
|
||||||
};
|
};
|
||||||
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
|
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
|
||||||
let message_text = format!("{}\r\n", p2);
|
let message_text = format!("{}\r\n", format_flowed(&p2));
|
||||||
message = message.child(
|
message = message.child(
|
||||||
PartBuilder::new()
|
PartBuilder::new()
|
||||||
.content_type(&mime::TEXT_PLAIN_UTF_8)
|
.header((
|
||||||
|
"Content-Type".to_string(),
|
||||||
|
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
||||||
|
))
|
||||||
.body(message_text)
|
.body(message_text)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
@@ -1194,29 +1280,11 @@ async fn build_body_file(
|
|||||||
Ok((mail, filename_to_send))
|
Ok((mail, filename_to_send))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String)> {
|
fn build_selfavatar_file(context: &Context, path: &str) -> Result<String> {
|
||||||
let blob = BlobObject::from_path(context, path)?;
|
let blob = BlobObject::from_path(context, path.as_ref())?;
|
||||||
let filename_to_send = match blob.suffix() {
|
|
||||||
Some(suffix) => format!("avatar.{}", suffix),
|
|
||||||
None => "avatar".to_string(),
|
|
||||||
};
|
|
||||||
let mimetype = match message::guess_msgtype_from_suffix(blob.as_rel_path()) {
|
|
||||||
Some(res) => res.1.parse()?,
|
|
||||||
None => mime::APPLICATION_OCTET_STREAM,
|
|
||||||
};
|
|
||||||
let body = std::fs::read(blob.to_abs_path())?;
|
let body = std::fs::read(blob.to_abs_path())?;
|
||||||
let encoded_body = wrapped_base64_encode(&body);
|
let encoded_body = wrapped_base64_encode(&body);
|
||||||
|
Ok(encoded_body)
|
||||||
let part = PartBuilder::new()
|
|
||||||
.content_type(&mimetype)
|
|
||||||
.header((
|
|
||||||
"Content-Disposition",
|
|
||||||
format!("attachment; filename=\"{}\"", &filename_to_send),
|
|
||||||
))
|
|
||||||
.header(("Content-Transfer-Encoding", "base64"))
|
|
||||||
.body(encoded_body);
|
|
||||||
|
|
||||||
Ok((part, filename_to_send))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool {
|
fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool {
|
||||||
@@ -1263,8 +1331,8 @@ fn encode_words(word: &str) -> String {
|
|||||||
encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None)
|
encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_encoding(to_check: impl AsRef<str>) -> bool {
|
fn needs_encoding(to_check: &str) -> bool {
|
||||||
!to_check.as_ref().chars().all(|c| {
|
!to_check.chars().all(|c| {
|
||||||
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' || c == '%'
|
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' || c == '%'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1280,14 +1348,16 @@ fn maybe_encode_words(words: &str) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::ChatId;
|
use async_std::prelude::*;
|
||||||
|
|
||||||
|
use crate::chat::ChatId;
|
||||||
|
use crate::chatlist::Chatlist;
|
||||||
use crate::contact::Origin;
|
use crate::contact::Origin;
|
||||||
use crate::dc_receive_imf::dc_receive_imf;
|
use crate::dc_receive_imf::dc_receive_imf;
|
||||||
use crate::mimeparser::MimeMessage;
|
use crate::mimeparser::MimeMessage;
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::{get_chat_msg, TestContext};
|
||||||
use crate::{chatlist::Chatlist, test_utils::get_chat_msg};
|
|
||||||
|
|
||||||
|
use async_std::fs::File;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1620,7 +1690,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let chat_id = chat::create_by_contact_id(&t, contact_id).await.unwrap();
|
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
|
||||||
|
|
||||||
let mut new_msg = Message::new(Viewtype::Text);
|
let mut new_msg = Message::new(Viewtype::Text);
|
||||||
new_msg.set_text(Some("Hi".to_string()));
|
new_msg.set_text(Some("Hi".to_string()));
|
||||||
@@ -1838,4 +1908,59 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!headers.lines().any(|l| l.trim().is_empty()));
|
assert!(!headers.lines().any(|l| l.trim().is_empty()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||||
|
// create chat with bob, set selfavatar
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
|
||||||
|
|
||||||
|
let file = t.dir.path().join("avatar.png");
|
||||||
|
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
|
File::create(&file).await?.write_all(bytes).await?;
|
||||||
|
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
|
||||||
|
// make sure, `Subject:` stays in the outer header (imf header)
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.set_text(Some("this is the text!".to_string()));
|
||||||
|
|
||||||
|
let payload = t.send_msg(chat.id, &mut msg).await.payload();
|
||||||
|
let mut payload = payload.splitn(3, "\r\n\r\n");
|
||||||
|
let outer = payload.next().unwrap();
|
||||||
|
let inner = payload.next().unwrap();
|
||||||
|
let body = payload.next().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("Subject:").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
|
||||||
|
|
||||||
|
assert_eq!(inner.match_indices("text/plain").count(), 1);
|
||||||
|
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
|
||||||
|
assert_eq!(inner.match_indices("Subject:").count(), 0);
|
||||||
|
|
||||||
|
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||||
|
|
||||||
|
// if another message is sent, that one must not contain the avatar
|
||||||
|
// and no artificial multipart/mixed nesting
|
||||||
|
let payload = t.send_msg(chat.id, &mut msg).await.payload();
|
||||||
|
let mut payload = payload.splitn(2, "\r\n\r\n");
|
||||||
|
let outer = payload.next().unwrap();
|
||||||
|
let body = payload.next().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(outer.match_indices("text/plain").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("Subject:").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||||
|
assert_eq!(outer.match_indices("multipart/mixed").count(), 0);
|
||||||
|
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
|
||||||
|
|
||||||
|
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||||
|
assert_eq!(body.match_indices("text/plain").count(), 0);
|
||||||
|
assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0);
|
||||||
|
assert_eq!(body.match_indices("Subject:").count(), 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::pin::Pin;
|
|||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use charset::Charset;
|
use charset::Charset;
|
||||||
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use lettre_email::mime::{self, Mime};
|
use lettre_email::mime::{self, Mime};
|
||||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
@@ -102,7 +103,9 @@ pub(crate) enum MailinglistType {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||||
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum SystemMessage {
|
pub enum SystemMessage {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -146,7 +149,7 @@ impl MimeMessage {
|
|||||||
let mut from = Default::default();
|
let mut from = Default::default();
|
||||||
let mut chat_disposition_notification_to = None;
|
let mut chat_disposition_notification_to = None;
|
||||||
|
|
||||||
// init known headers with what mailparse provided us
|
// Parse IMF headers.
|
||||||
MimeMessage::merge_headers(
|
MimeMessage::merge_headers(
|
||||||
context,
|
context,
|
||||||
&mut headers,
|
&mut headers,
|
||||||
@@ -156,6 +159,21 @@ impl MimeMessage {
|
|||||||
&mail.headers,
|
&mail.headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Parse hidden headers.
|
||||||
|
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||||
|
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
|
||||||
|
if let Some(part) = mail.subparts.first() {
|
||||||
|
for field in &part.headers {
|
||||||
|
let key = field.get_key().to_lowercase();
|
||||||
|
|
||||||
|
// For now only Chat-User-Avatar can be hidden.
|
||||||
|
if !headers.contains_key(&key) && key == "chat-user-avatar" {
|
||||||
|
headers.insert(key.to_string(), field.get_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// remove headers that are allowed _only_ in the encrypted part
|
// remove headers that are allowed _only_ in the encrypted part
|
||||||
headers.remove("secure-join-fingerprint");
|
headers.remove("secure-join-fingerprint");
|
||||||
headers.remove("chat-verified");
|
headers.remove("chat-verified");
|
||||||
@@ -260,7 +278,7 @@ impl MimeMessage {
|
|||||||
parser.maybe_remove_bad_parts();
|
parser.maybe_remove_bad_parts();
|
||||||
parser.maybe_remove_inline_mailinglist_footer();
|
parser.maybe_remove_inline_mailinglist_footer();
|
||||||
parser.heuristically_parse_ndn(context).await;
|
parser.heuristically_parse_ndn(context).await;
|
||||||
parser.parse_headers(context);
|
parser.parse_headers(context).await;
|
||||||
|
|
||||||
if warn_empty_signature && parser.signatures.is_empty() {
|
if warn_empty_signature && parser.signatures.is_empty() {
|
||||||
for part in parser.parts.iter_mut() {
|
for part in parser.parts.iter_mut() {
|
||||||
@@ -307,13 +325,13 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses avatar action headers.
|
/// Parses avatar action headers.
|
||||||
fn parse_avatar_headers(&mut self) {
|
async fn parse_avatar_headers(&mut self, context: &Context) {
|
||||||
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
|
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
|
||||||
self.group_avatar = self.avatar_action_from_header(header_value);
|
self.group_avatar = self.avatar_action_from_header(context, header_value).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
|
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
|
||||||
self.user_avatar = self.avatar_action_from_header(header_value);
|
self.user_avatar = self.avatar_action_from_header(context, header_value).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +421,9 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_headers(&mut self, context: &Context) {
|
async fn parse_headers(&mut self, context: &Context) {
|
||||||
self.parse_system_message_headers(context);
|
self.parse_system_message_headers(context);
|
||||||
self.parse_avatar_headers();
|
self.parse_avatar_headers(context).await;
|
||||||
self.parse_videochat_headers();
|
self.parse_videochat_headers();
|
||||||
self.squash_attachment_parts();
|
self.squash_attachment_parts();
|
||||||
|
|
||||||
@@ -482,10 +500,48 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn avatar_action_from_header(&mut self, header_value: String) -> Option<AvatarAction> {
|
async fn avatar_action_from_header(
|
||||||
|
&mut self,
|
||||||
|
context: &Context,
|
||||||
|
header_value: String,
|
||||||
|
) -> Option<AvatarAction> {
|
||||||
if header_value == "0" {
|
if header_value == "0" {
|
||||||
Some(AvatarAction::Delete)
|
Some(AvatarAction::Delete)
|
||||||
|
} else if let Some(avatar) = header_value
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.collect::<String>()
|
||||||
|
.strip_prefix("base64:")
|
||||||
|
.map(base64::decode)
|
||||||
|
{
|
||||||
|
// Avatar sent directly in the header as base64.
|
||||||
|
if let Ok(decoded_data) = avatar {
|
||||||
|
let extension = if let Ok(format) = image::guess_format(&decoded_data) {
|
||||||
|
if let Some(ext) = format.extensions_str().first() {
|
||||||
|
format!(".{}", ext)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
match BlobObject::create(context, &format!("avatar{}", extension), &decoded_data)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(blob) => Some(AvatarAction::Change(blob.as_name().to_string())),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
"Could not save decoded avatar to blob file: {}", err
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Avatar sent in attachment, as previous versions of Delta Chat did.
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while let Some(part) = self.parts.get_mut(i) {
|
while let Some(part) = self.parts.get_mut(i) {
|
||||||
if let Some(part_filename) = &part.org_filename {
|
if let Some(part_filename) = &part.org_filename {
|
||||||
@@ -1249,7 +1305,8 @@ impl MimeMessage {
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field),
|
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
|
||||||
|
paramsv![field],
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
@@ -1920,9 +1977,8 @@ mod tests {
|
|||||||
.ctx
|
.ctx
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)")
|
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
|
||||||
.bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org")
|
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
|
||||||
.bind(timestamp),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to the database");
|
.expect("Failed to write to the database");
|
||||||
|
|||||||
@@ -53,20 +53,20 @@ struct Response {
|
|||||||
|
|
||||||
pub async fn dc_get_oauth2_url(
|
pub async fn dc_get_oauth2_url(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
addr: impl AsRef<str>,
|
addr: &str,
|
||||||
redirect_uri: impl AsRef<str>,
|
redirect_uri: &str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
||||||
if context
|
if context
|
||||||
.sql
|
.sql
|
||||||
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri.as_ref()))
|
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let oauth2_url = replace_in_uri(&oauth2.get_code, "$CLIENT_ID", &oauth2.client_id);
|
let oauth2_url = replace_in_uri(oauth2.get_code, "$CLIENT_ID", oauth2.client_id);
|
||||||
let oauth2_url = replace_in_uri(&oauth2_url, "$REDIRECT_URI", redirect_uri.as_ref());
|
let oauth2_url = replace_in_uri(&oauth2_url, "$REDIRECT_URI", redirect_uri);
|
||||||
|
|
||||||
Some(oauth2_url)
|
Some(oauth2_url)
|
||||||
} else {
|
} else {
|
||||||
@@ -76,8 +76,8 @@ pub async fn dc_get_oauth2_url(
|
|||||||
|
|
||||||
pub async fn dc_get_oauth2_access_token(
|
pub async fn dc_get_oauth2_access_token(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
addr: impl AsRef<str>,
|
addr: &str,
|
||||||
code: impl AsRef<str>,
|
code: &str,
|
||||||
regenerate: bool,
|
regenerate: bool,
|
||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
||||||
@@ -101,7 +101,7 @@ pub async fn dc_get_oauth2_access_token(
|
|||||||
.unwrap_or_else(|| "unset".into());
|
.unwrap_or_else(|| "unset".into());
|
||||||
|
|
||||||
let (redirect_uri, token_url, update_redirect_uri_on_success) =
|
let (redirect_uri, token_url, update_redirect_uri_on_success) =
|
||||||
if refresh_token.is_none() || refresh_token_for != code.as_ref() {
|
if refresh_token.is_none() || refresh_token_for != code {
|
||||||
info!(context, "Generate OAuth2 refresh_token and access_token...",);
|
info!(context, "Generate OAuth2 refresh_token and access_token...",);
|
||||||
(
|
(
|
||||||
context
|
context
|
||||||
@@ -145,7 +145,7 @@ pub async fn dc_get_oauth2_access_token(
|
|||||||
} else if value == "$REDIRECT_URI" {
|
} else if value == "$REDIRECT_URI" {
|
||||||
value = &redirect_uri;
|
value = &redirect_uri;
|
||||||
} else if value == "$CODE" {
|
} else if value == "$CODE" {
|
||||||
value = code.as_ref();
|
value = code;
|
||||||
} else if value == "$REFRESH_TOKEN" && refresh_token.is_some() {
|
} else if value == "$REFRESH_TOKEN" && refresh_token.is_some() {
|
||||||
value = refresh_token.as_ref().unwrap();
|
value = refresh_token.as_ref().unwrap();
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ pub async fn dc_get_oauth2_access_token(
|
|||||||
.await?;
|
.await?;
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.set_raw_config("oauth2_refresh_token_for", Some(code.as_ref()))
|
.set_raw_config("oauth2_refresh_token_for", Some(code))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,10 +222,10 @@ pub async fn dc_get_oauth2_access_token(
|
|||||||
|
|
||||||
pub async fn dc_get_oauth2_addr(
|
pub async fn dc_get_oauth2_addr(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
addr: impl AsRef<str>,
|
addr: &str,
|
||||||
code: impl AsRef<str>,
|
code: &str,
|
||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
let oauth2 = match Oauth2::from_address(addr.as_ref()).await {
|
let oauth2 = match Oauth2::from_address(addr).await {
|
||||||
Some(o) => o,
|
Some(o) => o,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
@@ -233,16 +233,14 @@ pub async fn dc_get_oauth2_addr(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(access_token) =
|
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, false).await? {
|
||||||
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await?
|
let addr_out = oauth2.get_addr(context, &access_token).await;
|
||||||
{
|
|
||||||
let addr_out = oauth2.get_addr(context, access_token).await;
|
|
||||||
if addr_out.is_none() {
|
if addr_out.is_none() {
|
||||||
// regenerate
|
// regenerate
|
||||||
if let Some(access_token) =
|
if let Some(access_token) =
|
||||||
dc_get_oauth2_access_token(context, addr, code, true).await?
|
dc_get_oauth2_access_token(context, addr, code, true).await?
|
||||||
{
|
{
|
||||||
Ok(oauth2.get_addr(context, access_token).await)
|
Ok(oauth2.get_addr(context, &access_token).await)
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -255,8 +253,8 @@ pub async fn dc_get_oauth2_addr(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Oauth2 {
|
impl Oauth2 {
|
||||||
async fn from_address(addr: impl AsRef<str>) -> Option<Self> {
|
async fn from_address(addr: &str) -> Option<Self> {
|
||||||
let addr_normalized = normalize_addr(addr.as_ref());
|
let addr_normalized = normalize_addr(addr);
|
||||||
if let Some(domain) = addr_normalized
|
if let Some(domain) = addr_normalized
|
||||||
.find('@')
|
.find('@')
|
||||||
.map(|index| addr_normalized.split_at(index + 1).1)
|
.map(|index| addr_normalized.split_at(index + 1).1)
|
||||||
@@ -274,9 +272,9 @@ impl Oauth2 {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
|
async fn get_addr(&self, context: &Context, access_token: &str) -> Option<String> {
|
||||||
let userinfo_url = self.get_userinfo.unwrap_or("");
|
let userinfo_url = self.get_userinfo.unwrap_or("");
|
||||||
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
|
let userinfo_url = replace_in_uri(userinfo_url, "$ACCESS_TOKEN", access_token);
|
||||||
|
|
||||||
// should returns sth. as
|
// should returns sth. as
|
||||||
// {
|
// {
|
||||||
@@ -309,7 +307,7 @@ impl Oauth2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_expired(context: &Context) -> Result<bool, crate::sql::Error> {
|
async fn is_expired(context: &Context) -> Result<bool> {
|
||||||
let expire_timestamp = context
|
let expire_timestamp = context
|
||||||
.sql
|
.sql
|
||||||
.get_raw_config_int64("oauth2_timestamp_expires")
|
.get_raw_config_int64("oauth2_timestamp_expires")
|
||||||
@@ -326,9 +324,9 @@ async fn is_expired(context: &Context) -> Result<bool, crate::sql::Error> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_in_uri(uri: impl AsRef<str>, key: impl AsRef<str>, value: impl AsRef<str>) -> String {
|
fn replace_in_uri(uri: &str, key: &str, value: &str) -> String {
|
||||||
let value_urlencoded = utf8_percent_encode(value.as_ref(), NON_ALPHANUMERIC).to_string();
|
let value_urlencoded = utf8_percent_encode(value, NON_ALPHANUMERIC).to_string();
|
||||||
uri.as_ref().replace(key.as_ref(), &value_urlencoded)
|
uri.replace(key, &value_urlencoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_addr(addr: &str) -> &str {
|
fn normalize_addr(addr: &str) -> &str {
|
||||||
|
|||||||
@@ -306,8 +306,8 @@ impl Params {
|
|||||||
let file = ParamsFile::from_param(context, val)?;
|
let file = ParamsFile::from_param(context, val)?;
|
||||||
let blob = match file {
|
let blob = match file {
|
||||||
ParamsFile::FsPath(path) => match create {
|
ParamsFile::FsPath(path) => match create {
|
||||||
true => BlobObject::new_from_path(context, path).await?,
|
true => BlobObject::new_from_path(context, &path).await?,
|
||||||
false => BlobObject::from_path(context, path)?,
|
false => BlobObject::from_path(context, &path)?,
|
||||||
},
|
},
|
||||||
ParamsFile::Blob(blob) => blob,
|
ParamsFile::Blob(blob) => blob,
|
||||||
};
|
};
|
||||||
|
|||||||
213
src/peerstate.rs
213
src/peerstate.rs
@@ -3,18 +3,16 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use num_traits::FromPrimitive;
|
|
||||||
use sqlx::{query::Query, sqlite::Sqlite, Row};
|
|
||||||
|
|
||||||
use crate::aheader::{Aheader, EncryptPreference};
|
use crate::aheader::{Aheader, EncryptPreference};
|
||||||
use crate::chat;
|
use crate::chat::{self, ChatIdBlocked};
|
||||||
use crate::constants::Blocked;
|
use crate::constants::Blocked;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||||
use crate::sql::Sql;
|
use crate::sql::Sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeerstateKeyType {
|
pub enum PeerstateKeyType {
|
||||||
@@ -140,15 +138,12 @@ impl Peerstate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
|
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
|
||||||
let query = sqlx::query(
|
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||||
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
|
||||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||||
verified_key, verified_key_fingerprint \
|
verified_key, verified_key_fingerprint \
|
||||||
FROM acpeerstates \
|
FROM acpeerstates \
|
||||||
WHERE addr=? COLLATE NOCASE;",
|
WHERE addr=? COLLATE NOCASE;";
|
||||||
)
|
Self::from_stmt(context, query, paramsv![addr]).await
|
||||||
.bind(addr);
|
|
||||||
Self::from_stmt(context, query).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_fingerprint(
|
pub async fn from_fingerprint(
|
||||||
@@ -156,77 +151,71 @@ impl Peerstate {
|
|||||||
_sql: &Sql,
|
_sql: &Sql,
|
||||||
fingerprint: &Fingerprint,
|
fingerprint: &Fingerprint,
|
||||||
) -> Result<Option<Peerstate>> {
|
) -> Result<Option<Peerstate>> {
|
||||||
let fp = fingerprint.hex();
|
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||||
let query = sqlx::query(
|
|
||||||
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
|
||||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||||
verified_key, verified_key_fingerprint \
|
verified_key, verified_key_fingerprint \
|
||||||
FROM acpeerstates \
|
FROM acpeerstates \
|
||||||
WHERE public_key_fingerprint=? COLLATE NOCASE \
|
WHERE public_key_fingerprint=? COLLATE NOCASE \
|
||||||
OR gossip_key_fingerprint=? COLLATE NOCASE \
|
OR gossip_key_fingerprint=? COLLATE NOCASE \
|
||||||
ORDER BY public_key_fingerprint=? DESC;",
|
ORDER BY public_key_fingerprint=? DESC;";
|
||||||
)
|
let fp = fingerprint.hex();
|
||||||
.bind(&fp)
|
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
|
||||||
.bind(&fp)
|
|
||||||
.bind(&fp);
|
|
||||||
|
|
||||||
Self::from_stmt(context, query).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn from_stmt<'q, E>(
|
async fn from_stmt(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
query: Query<'q, Sqlite, E>,
|
query: &str,
|
||||||
) -> Result<Option<Peerstate>>
|
params: impl rusqlite::Params,
|
||||||
where
|
) -> Result<Option<Peerstate>> {
|
||||||
E: 'q + sqlx::IntoArguments<'q, sqlx::Sqlite>,
|
let peerstate = context
|
||||||
{
|
.sql
|
||||||
if let Some(row) = context.sql.fetch_optional(query).await? {
|
.query_row_optional(query, params, |row| {
|
||||||
// all the above queries start with this: SELECT
|
// all the above queries start with this: SELECT
|
||||||
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||||
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||||
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||||
|
|
||||||
let peerstate = Peerstate {
|
let res = Peerstate {
|
||||||
addr: row.try_get(0)?,
|
addr: row.get(0)?,
|
||||||
last_seen: row.try_get(1)?,
|
last_seen: row.get(1)?,
|
||||||
last_seen_autocrypt: row.try_get(2)?,
|
last_seen_autocrypt: row.get(2)?,
|
||||||
prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(),
|
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
|
||||||
public_key: row
|
public_key: row
|
||||||
.try_get::<&[u8], _>(4)
|
.get(4)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||||
public_key_fingerprint: row
|
public_key_fingerprint: row
|
||||||
.try_get::<Option<String>, _>(7)?
|
.get::<_, Option<String>>(7)?
|
||||||
.map(|s| s.parse::<Fingerprint>())
|
.map(|s| s.parse::<Fingerprint>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
gossip_key: row
|
gossip_key: row
|
||||||
.try_get::<&[u8], _>(6)
|
.get(6)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||||
gossip_key_fingerprint: row
|
gossip_key_fingerprint: row
|
||||||
.try_get::<Option<String>, _>(8)?
|
.get::<_, Option<String>>(8)?
|
||||||
.map(|s| s.parse::<Fingerprint>())
|
.map(|s| s.parse::<Fingerprint>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
gossip_timestamp: row.try_get(5)?,
|
gossip_timestamp: row.get(5)?,
|
||||||
verified_key: row
|
verified_key: row
|
||||||
.try_get::<&[u8], _>(9)
|
.get(9)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||||
verified_key_fingerprint: row
|
verified_key_fingerprint: row
|
||||||
.try_get::<Option<String>, _>(10)?
|
.get::<_, Option<String>>(10)?
|
||||||
.map(|s| s.parse::<Fingerprint>())
|
.map(|s| s.parse::<Fingerprint>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
to_save: None,
|
to_save: None,
|
||||||
fingerprint_changed: false,
|
fingerprint_changed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(peerstate))
|
Ok(res)
|
||||||
} else {
|
})
|
||||||
Ok(None)
|
.await?;
|
||||||
}
|
Ok(peerstate)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recalc_fingerprint(&mut self) {
|
pub fn recalc_fingerprint(&mut self) {
|
||||||
@@ -275,20 +264,18 @@ impl Peerstate {
|
|||||||
if self.fingerprint_changed {
|
if self.fingerprint_changed {
|
||||||
if let Some(contact_id) = context
|
if let Some(contact_id) = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value("SELECT id FROM contacts WHERE addr=?;", paramsv![self.addr])
|
||||||
sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr),
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
let (contact_chat_id, _) =
|
let chat_id =
|
||||||
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Deaddrop)
|
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Deaddrop)
|
||||||
.await
|
.await?
|
||||||
.unwrap_or_default();
|
.id;
|
||||||
|
|
||||||
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
|
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
|
||||||
|
|
||||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
chat::add_info_msg(context, chat_id, msg).await;
|
||||||
emit_event!(context, EventType::ChatModified(contact_chat_id));
|
emit_event!(context, EventType::ChatModified(chat_id));
|
||||||
} else {
|
} else {
|
||||||
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
||||||
}
|
}
|
||||||
@@ -434,12 +421,11 @@ impl Peerstate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
|
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> Result<()> {
|
||||||
if self.to_save == Some(ToSave::All) || create {
|
if self.to_save == Some(ToSave::All) || create {
|
||||||
sql.execute(
|
sql.execute(
|
||||||
(if create {
|
if create {
|
||||||
sqlx::query(
|
"INSERT INTO acpeerstates ( \
|
||||||
"INSERT INTO acpeerstates ( \
|
|
||||||
last_seen, \
|
last_seen, \
|
||||||
last_seen_autocrypt, \
|
last_seen_autocrypt, \
|
||||||
prefer_encrypted, \
|
prefer_encrypted, \
|
||||||
@@ -451,11 +437,9 @@ impl Peerstate {
|
|||||||
verified_key, \
|
verified_key, \
|
||||||
verified_key_fingerprint, \
|
verified_key_fingerprint, \
|
||||||
addr \
|
addr \
|
||||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
sqlx::query(
|
"UPDATE acpeerstates \
|
||||||
"UPDATE acpeerstates \
|
|
||||||
SET last_seen=?, \
|
SET last_seen=?, \
|
||||||
last_seen_autocrypt=?, \
|
last_seen_autocrypt=?, \
|
||||||
prefer_encrypted=?, \
|
prefer_encrypted=?, \
|
||||||
@@ -466,30 +450,33 @@ impl Peerstate {
|
|||||||
gossip_key_fingerprint=?, \
|
gossip_key_fingerprint=?, \
|
||||||
verified_key=?, \
|
verified_key=?, \
|
||||||
verified_key_fingerprint=? \
|
verified_key_fingerprint=? \
|
||||||
WHERE addr=?",
|
WHERE addr=?"
|
||||||
)
|
},
|
||||||
})
|
paramsv![
|
||||||
.bind(self.last_seen)
|
self.last_seen,
|
||||||
.bind(self.last_seen_autocrypt)
|
self.last_seen_autocrypt,
|
||||||
.bind(self.prefer_encrypt as i64)
|
self.prefer_encrypt as i64,
|
||||||
.bind(self.public_key.as_ref().map(|k| k.to_bytes()))
|
self.public_key.as_ref().map(|k| k.to_bytes()),
|
||||||
.bind(self.gossip_timestamp)
|
self.gossip_timestamp,
|
||||||
.bind(self.gossip_key.as_ref().map(|k| k.to_bytes()))
|
self.gossip_key.as_ref().map(|k| k.to_bytes()),
|
||||||
.bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||||
.bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||||
.bind(self.verified_key.as_ref().map(|k| k.to_bytes()))
|
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||||
.bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||||
.bind(&self.addr),
|
self.addr,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else if self.to_save == Some(ToSave::Timestamps) {
|
} else if self.to_save == Some(ToSave::Timestamps) {
|
||||||
sql.execute(
|
sql.execute(
|
||||||
sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
||||||
WHERE addr=?;").bind(
|
WHERE addr=?;",
|
||||||
self.last_seen).bind(
|
paramsv![
|
||||||
self.last_seen_autocrypt).bind(
|
self.last_seen,
|
||||||
self.gossip_timestamp).bind(
|
self.last_seen_autocrypt,
|
||||||
&self.addr)
|
self.gossip_timestamp,
|
||||||
|
self.addr
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -506,6 +493,12 @@ impl Peerstate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::key::FingerprintError> for rusqlite::Error {
|
||||||
|
fn from(_source: crate::key::FingerprintError) -> Self {
|
||||||
|
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -638,7 +631,7 @@ mod tests {
|
|||||||
// can be loaded without errors.
|
// can be loaded without errors.
|
||||||
ctx.ctx
|
ctx.ctx
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr))
|
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to the database");
|
.expect("Failed to write to the database");
|
||||||
|
|
||||||
|
|||||||
@@ -115,14 +115,14 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
|
|||||||
/// Finds a provider based on MX record for the given domain.
|
/// Finds a provider based on MX record for the given domain.
|
||||||
///
|
///
|
||||||
/// For security reasons, only Gmail can be configured this way.
|
/// For security reasons, only Gmail can be configured this way.
|
||||||
pub async fn get_provider_by_mx(domain: impl AsRef<str>) -> Option<&'static Provider> {
|
pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
|
||||||
if let Ok(resolver) = resolver(
|
if let Ok(resolver) = resolver(
|
||||||
config::ResolverConfig::default(),
|
config::ResolverConfig::default(),
|
||||||
config::ResolverOpts::default(),
|
config::ResolverOpts::default(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let mut fqdn: String = String::from(domain.as_ref());
|
let mut fqdn: String = domain.to_string();
|
||||||
if !fqdn.ends_with('.') {
|
if !fqdn.ends_with('.') {
|
||||||
fqdn.push('.');
|
fqdn.push('.');
|
||||||
}
|
}
|
||||||
36
src/qr.rs
36
src/qr.rs
@@ -6,12 +6,13 @@ use percent_encoding::percent_decode_str;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::chat;
|
use crate::chat::{self, ChatIdBlocked};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::Blocked;
|
use crate::constants::Blocked;
|
||||||
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
|
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::key::Fingerprint;
|
use crate::key::Fingerprint;
|
||||||
|
use crate::log::LogExt;
|
||||||
use crate::lot::{Lot, LotState};
|
use crate::lot::{Lot, LotState};
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
@@ -44,9 +45,7 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
|||||||
/// Check a scanned QR code.
|
/// Check a scanned QR code.
|
||||||
/// The function should be called after a QR code is scanned.
|
/// The function should be called after a QR code is scanned.
|
||||||
/// The function takes the raw text scanned and checks what can be done with it.
|
/// The function takes the raw text scanned and checks what can be done with it.
|
||||||
pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
pub async fn check_qr(context: &Context, qr: &str) -> Lot {
|
||||||
let qr = qr.as_ref();
|
|
||||||
|
|
||||||
info!(context, "Scanned QR code: {}", qr);
|
info!(context, "Scanned QR code: {}", qr);
|
||||||
|
|
||||||
if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
||||||
@@ -155,21 +154,18 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
|||||||
if let Some(peerstate) = peerstate {
|
if let Some(peerstate) = peerstate {
|
||||||
lot.state = LotState::QrFprOk;
|
lot.state = LotState::QrFprOk;
|
||||||
|
|
||||||
lot.id = Contact::add_or_lookup(
|
lot.id =
|
||||||
context,
|
Contact::add_or_lookup(context, &name, &peerstate.addr, Origin::UnhandledQrScan)
|
||||||
name,
|
.await
|
||||||
peerstate.addr.clone(),
|
.map(|(id, _)| id)
|
||||||
Origin::UnhandledQrScan,
|
.unwrap_or_default();
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|(id, _)| id)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop)
|
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, lot.id, Blocked::Deaddrop)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.log_err(context, "Failed to create (new) chat for contact")
|
||||||
|
{
|
||||||
chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await;
|
chat::add_info_msg(context, chat.id, format!("{} verified.", peerstate.addr)).await;
|
||||||
|
}
|
||||||
} else if let Some(addr) = addr {
|
} else if let Some(addr) = addr {
|
||||||
lot.state = LotState::QrFprMismatch;
|
lot.state = LotState::QrFprMismatch;
|
||||||
lot.id = match Contact::lookup_id_by_addr(context, &addr, Origin::Unknown).await {
|
lot.id = match Contact::lookup_id_by_addr(context, &addr, Origin::Unknown).await {
|
||||||
@@ -284,7 +280,7 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||||
match check_qr(context, &qr).await.state {
|
match check_qr(context, qr).await.state {
|
||||||
LotState::QrAccount => set_account_from_qr(context, qr).await,
|
LotState::QrAccount => set_account_from_qr(context, qr).await,
|
||||||
LotState::QrWebrtcInstance => {
|
LotState::QrWebrtcInstance => {
|
||||||
let val = decode_webrtc_instance(context, qr).text2;
|
let val = decode_webrtc_instance(context, qr).text2;
|
||||||
@@ -421,7 +417,7 @@ impl Lot {
|
|||||||
pub async fn from_address(context: &Context, name: String, addr: String) -> Self {
|
pub async fn from_address(context: &Context, name: String, addr: String) -> Self {
|
||||||
let mut l = Lot::new();
|
let mut l = Lot::new();
|
||||||
l.state = LotState::QrAddr;
|
l.state = LotState::QrAddr;
|
||||||
l.id = match Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await {
|
l.id = match Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await {
|
||||||
Ok((id, _)) => id,
|
Ok((id, _)) => id,
|
||||||
Err(err) => return err.into(),
|
Err(err) => return err.into(),
|
||||||
};
|
};
|
||||||
@@ -675,7 +671,7 @@ mod tests {
|
|||||||
|
|
||||||
let res = check_qr(
|
let res = check_qr(
|
||||||
&ctx.ctx,
|
&ctx.ctx,
|
||||||
format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
|
&format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(res.get_state(), LotState::QrFprOk);
|
assert_eq!(res.get_state(), LotState::QrFprOk);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use async_std::sync::Mutex;
|
|||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||||
|
|
||||||
use crate::aheader::EncryptPreference;
|
use crate::aheader::EncryptPreference;
|
||||||
use crate::chat::{self, Chat, ChatId};
|
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||||
use crate::contact::{Contact, Origin, VerifiedStatus};
|
use crate::contact::{Contact, Origin, VerifiedStatus};
|
||||||
@@ -23,7 +23,6 @@ use crate::mimeparser::{MimeMessage, SystemMessage};
|
|||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
|
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
|
||||||
use crate::qr::check_qr;
|
use crate::qr::check_qr;
|
||||||
use crate::sql;
|
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::token;
|
use crate::token;
|
||||||
|
|
||||||
@@ -267,8 +266,6 @@ pub enum JoinError {
|
|||||||
#[error("Unknown contact (this is a bug)")]
|
#[error("Unknown contact (this is a bug)")]
|
||||||
UnknownContact(#[source] anyhow::Error),
|
UnknownContact(#[source] anyhow::Error),
|
||||||
// Note that this can only occur if we failed to create the chat correctly.
|
// Note that this can only occur if we failed to create the chat correctly.
|
||||||
#[error("No Chat found for group (this is a bug)")]
|
|
||||||
MissingChat(#[source] sql::Error),
|
|
||||||
#[error("Ongoing sender dropped (this is a bug)")]
|
#[error("Ongoing sender dropped (this is a bug)")]
|
||||||
OngoingSenderDropped,
|
OngoingSenderDropped,
|
||||||
#[error("Other")]
|
#[error("Other")]
|
||||||
@@ -299,7 +296,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
|||||||
========================================================*/
|
========================================================*/
|
||||||
|
|
||||||
info!(context, "Requesting secure-join ...",);
|
info!(context, "Requesting secure-join ...",);
|
||||||
let qr_scan = check_qr(context, &qr).await;
|
let qr_scan = check_qr(context, qr).await;
|
||||||
|
|
||||||
let invite = QrInvite::try_from(qr_scan)?;
|
let invite = QrInvite::try_from(qr_scan)?;
|
||||||
|
|
||||||
@@ -307,7 +304,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
|||||||
StartedProtocolVariant::SetupContact => {
|
StartedProtocolVariant::SetupContact => {
|
||||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||||
// the verification runs in background
|
// the verification runs in background
|
||||||
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
|
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
|
||||||
.await
|
.await
|
||||||
.map_err(JoinError::UnknownContact)?;
|
.map_err(JoinError::UnknownContact)?;
|
||||||
Ok(chat_id)
|
Ok(chat_id)
|
||||||
@@ -335,7 +332,9 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
if start.elapsed() > Duration::from_secs(7) {
|
if start.elapsed() > Duration::from_secs(7) {
|
||||||
context.free_ongoing().await;
|
context.free_ongoing().await;
|
||||||
return Err(JoinError::MissingChat(err));
|
return Err(err
|
||||||
|
.context("Ongoing sender dropped (this is a bug)")
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,9 +365,9 @@ async fn send_handshake_msg(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
contact_chat_id: ChatId,
|
contact_chat_id: ChatId,
|
||||||
step: &str,
|
step: &str,
|
||||||
param2: impl AsRef<str>,
|
param2: &str,
|
||||||
fingerprint: Option<Fingerprint>,
|
fingerprint: Option<Fingerprint>,
|
||||||
grpid: impl AsRef<str>,
|
grpid: &str,
|
||||||
) -> Result<(), SendMsgError> {
|
) -> Result<(), SendMsgError> {
|
||||||
let mut msg = Message {
|
let mut msg = Message {
|
||||||
viewtype: Viewtype::Text,
|
viewtype: Viewtype::Text,
|
||||||
@@ -382,14 +381,14 @@ async fn send_handshake_msg(
|
|||||||
} else {
|
} else {
|
||||||
msg.param.set(Param::Arg, step);
|
msg.param.set(Param::Arg, step);
|
||||||
}
|
}
|
||||||
if !param2.as_ref().is_empty() {
|
if !param2.is_empty() {
|
||||||
msg.param.set(Param::Arg2, param2);
|
msg.param.set(Param::Arg2, param2);
|
||||||
}
|
}
|
||||||
if let Some(fp) = fingerprint {
|
if let Some(fp) = fingerprint {
|
||||||
msg.param.set(Param::Arg3, fp.hex());
|
msg.param.set(Param::Arg3, fp.hex());
|
||||||
}
|
}
|
||||||
if !grpid.as_ref().is_empty() {
|
if !grpid.is_empty() {
|
||||||
msg.param.set(Param::Arg4, grpid.as_ref());
|
msg.param.set(Param::Arg4, grpid);
|
||||||
}
|
}
|
||||||
if step == "vg-request" || step == "vc-request" {
|
if step == "vg-request" || step == "vc-request" {
|
||||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||||
@@ -499,19 +498,18 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let contact_chat_id = {
|
let contact_chat_id = {
|
||||||
let (chat_id, blocked) =
|
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||||
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not)
|
.await
|
||||||
.await
|
.with_context(|| {
|
||||||
.with_context(|| {
|
format!(
|
||||||
format!(
|
"Failed to look up or create chat for contact {}",
|
||||||
"Failed to look up or create chat for contact {}",
|
contact_id
|
||||||
contact_id
|
)
|
||||||
)
|
})?;
|
||||||
})?;
|
if chat.blocked != Blocked::Not {
|
||||||
if blocked != Blocked::Not {
|
chat.id.unblock(context).await;
|
||||||
chat_id.unblock(context).await;
|
|
||||||
}
|
}
|
||||||
chat_id
|
chat.id
|
||||||
};
|
};
|
||||||
|
|
||||||
let join_vg = step.starts_with("vg-");
|
let join_vg = step.starts_with("vg-");
|
||||||
@@ -669,8 +667,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(context, "Chat {} not found: {}", &field_grpid, err);
|
error!(context, "Chat {} not found: {}", &field_grpid, err);
|
||||||
return Err(Error::new(err)
|
return Err(
|
||||||
.context(format!("Chat for group {} not found", &field_grpid)));
|
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -745,8 +744,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
.unwrap_or_else(|| "");
|
.unwrap_or_else(|| "");
|
||||||
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
|
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
|
||||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||||
return Err(Error::new(err)
|
return Err(
|
||||||
.context(format!("Chat for group {} not found", &field_grpid)));
|
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
||||||
@@ -793,19 +793,18 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
|||||||
info!(context, "observing secure-join message \'{}\'", step);
|
info!(context, "observing secure-join message \'{}\'", step);
|
||||||
|
|
||||||
let contact_chat_id = {
|
let contact_chat_id = {
|
||||||
let (chat_id, blocked) =
|
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||||
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not)
|
.await
|
||||||
.await
|
.with_context(|| {
|
||||||
.with_context(|| {
|
format!(
|
||||||
format!(
|
"Failed to look up or create chat for contact {}",
|
||||||
"Failed to look up or create chat for contact {}",
|
contact_id
|
||||||
contact_id
|
)
|
||||||
)
|
})?;
|
||||||
})?;
|
if chat.blocked != Blocked::Not {
|
||||||
if blocked != Blocked::Not {
|
chat.id.unblock(context).await;
|
||||||
chat_id.unblock(context).await;
|
|
||||||
}
|
}
|
||||||
chat_id
|
chat.id
|
||||||
};
|
};
|
||||||
|
|
||||||
match step.as_str() {
|
match step.as_str() {
|
||||||
@@ -185,7 +185,7 @@ impl BobState {
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
invite: QrInvite,
|
invite: QrInvite,
|
||||||
) -> Result<(Self, BobHandshakeStage), JoinError> {
|
) -> Result<(Self, BobHandshakeStage), JoinError> {
|
||||||
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
|
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
|
||||||
.await
|
.await
|
||||||
.map_err(JoinError::UnknownContact)?;
|
.map_err(JoinError::UnknownContact)?;
|
||||||
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
|
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ pub enum Error {
|
|||||||
Oauth2Error { address: String },
|
Oauth2Error { address: String },
|
||||||
#[error("TLS error {0}")]
|
#[error("TLS error {0}")]
|
||||||
Tls(#[from] async_native_tls::Error),
|
Tls(#[from] async_native_tls::Error),
|
||||||
#[error("Sql {0}")]
|
|
||||||
Sql(#[from] crate::sql::Error),
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,17 @@
|
|||||||
//! # SQLite wrapper
|
//! # SQLite wrapper
|
||||||
|
|
||||||
|
use async_std::path::Path;
|
||||||
|
use async_std::sync::RwLock;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::convert::TryFrom;
|
||||||
use std::pin::Pin;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::{bail, format_err, Context as _, Result};
|
||||||
use async_std::prelude::*;
|
use async_std::prelude::*;
|
||||||
use async_std::sync::RwLock;
|
use rusqlite::OpenFlags;
|
||||||
use sqlx::{
|
|
||||||
pool::PoolOptions,
|
|
||||||
query::Query,
|
|
||||||
sqlite::{Sqlite, SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
|
|
||||||
Executor, IntoArguments, Row,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
|
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
|
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
|
||||||
@@ -26,38 +23,32 @@ use crate::param::{Param, Params};
|
|||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
|
|
||||||
mod error;
|
#[macro_export]
|
||||||
|
macro_rules! paramsv {
|
||||||
|
() => {
|
||||||
|
rusqlite::params_from_iter(Vec::<&dyn $crate::ToSql>::new())
|
||||||
|
};
|
||||||
|
($($param:expr),+ $(,)?) => {
|
||||||
|
rusqlite::params_from_iter(vec![$(&$param as &dyn $crate::ToSql),+])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
mod migrations;
|
mod migrations;
|
||||||
|
|
||||||
pub use self::error::*;
|
|
||||||
|
|
||||||
/// A wrapper around the underlying Sqlite3 object.
|
/// A wrapper around the underlying Sqlite3 object.
|
||||||
///
|
|
||||||
/// We maintain two different pools to sqlite, on for reading, one for writing.
|
|
||||||
/// This can go away once https://github.com/launchbadge/sqlx/issues/459 is implemented.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Sql {
|
pub struct Sql {
|
||||||
/// Writer pool, must only have 1 connection in it.
|
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
|
||||||
writer: RwLock<Option<SqlitePool>>,
|
|
||||||
/// Reader pool, maintains multiple connections for reading data.
|
|
||||||
reader: RwLock<Option<SqlitePool>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Sql {
|
impl Default for Sql {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
writer: RwLock::new(None),
|
pool: RwLock::new(None),
|
||||||
reader: RwLock::new(None),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Sql {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
async_std::task::block_on(self.close());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sql {
|
impl Sql {
|
||||||
pub fn new() -> Sql {
|
pub fn new() -> Sql {
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -65,76 +56,50 @@ impl Sql {
|
|||||||
|
|
||||||
/// Checks if there is currently a connection to the underlying Sqlite database.
|
/// Checks if there is currently a connection to the underlying Sqlite database.
|
||||||
pub async fn is_open(&self) -> bool {
|
pub async fn is_open(&self) -> bool {
|
||||||
// in read only mode the writer does not exists
|
self.pool.read().await.is_some()
|
||||||
self.reader.read().await.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes all underlying Sqlite connections.
|
/// Closes all underlying Sqlite connections.
|
||||||
pub async fn close(&self) {
|
pub async fn close(&self) {
|
||||||
if let Some(sql) = self.writer.write().await.take() {
|
let _ = self.pool.write().await.take();
|
||||||
sql.close().await;
|
// drop closes the connection
|
||||||
}
|
|
||||||
if let Some(sql) = self.reader.write().await.take() {
|
|
||||||
sql.close().await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn new_writer_pool(dbfile: impl AsRef<Path>) -> sqlx::Result<SqlitePool> {
|
pub fn new_pool(
|
||||||
let config = SqliteConnectOptions::new()
|
dbfile: &Path,
|
||||||
.journal_mode(SqliteJournalMode::Wal)
|
readonly: bool,
|
||||||
.filename(dbfile.as_ref())
|
) -> anyhow::Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
|
||||||
.read_only(false)
|
let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||||
.busy_timeout(Duration::from_secs(100))
|
if readonly {
|
||||||
.create_if_missing(true)
|
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY);
|
||||||
.shared_cache(true)
|
} else {
|
||||||
.synchronous(SqliteSynchronous::Normal);
|
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
|
||||||
|
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
PoolOptions::<Sqlite>::new()
|
// this actually creates min_idle database handles just now.
|
||||||
.max_connections(1)
|
// therefore, with_init() must not try to modify the database as otherwise
|
||||||
.after_connect(|conn| {
|
// we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle)
|
||||||
Box::pin(async move {
|
let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile)
|
||||||
let q = r#"
|
.with_flags(open_flags)
|
||||||
PRAGMA secure_delete=on;
|
.with_init(|c| {
|
||||||
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
c.execute_batch(&format!(
|
||||||
"#;
|
"PRAGMA secure_delete=on;
|
||||||
|
PRAGMA busy_timeout = {};
|
||||||
|
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
||||||
|
",
|
||||||
|
Duration::from_secs(10).as_millis()
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
conn.execute_many(sqlx::query(q))
|
let pool = r2d2::Pool::builder()
|
||||||
.collect::<std::result::Result<Vec<_>, _>>()
|
.min_idle(Some(2))
|
||||||
.await?;
|
.max_size(10)
|
||||||
Ok(())
|
.connection_timeout(Duration::from_secs(60))
|
||||||
})
|
.build(mgr)
|
||||||
})
|
.context("Can't build SQL connection pool")?;
|
||||||
.connect_with(config)
|
Ok(pool)
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new_reader_pool(dbfile: impl AsRef<Path>, readonly: bool) -> sqlx::Result<SqlitePool> {
|
|
||||||
let config = SqliteConnectOptions::new()
|
|
||||||
.journal_mode(SqliteJournalMode::Wal)
|
|
||||||
.filename(dbfile.as_ref())
|
|
||||||
.read_only(readonly)
|
|
||||||
.shared_cache(true)
|
|
||||||
.busy_timeout(Duration::from_secs(100))
|
|
||||||
.synchronous(SqliteSynchronous::Normal);
|
|
||||||
|
|
||||||
PoolOptions::<Sqlite>::new()
|
|
||||||
.max_connections(10)
|
|
||||||
.after_connect(|conn| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let q = r#"
|
|
||||||
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
|
||||||
PRAGMA query_only=1; -- Protect against writes even in read-write mode
|
|
||||||
PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared cache mode
|
|
||||||
"#;
|
|
||||||
|
|
||||||
conn.execute_many(sqlx::query(q))
|
|
||||||
.collect::<std::result::Result<Vec<_>, _>>()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.connect_with(config)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the provided database and runs any necessary migrations.
|
/// Opens the provided database and runs any necessary migrations.
|
||||||
@@ -142,32 +107,34 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c
|
|||||||
pub async fn open(
|
pub async fn open(
|
||||||
&self,
|
&self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
dbfile: impl AsRef<Path>,
|
dbfile: &Path,
|
||||||
readonly: bool,
|
readonly: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if self.is_open().await {
|
if self.is_open().await {
|
||||||
error!(
|
error!(
|
||||||
context,
|
context,
|
||||||
"Cannot open, database \"{:?}\" already opened.",
|
"Cannot open, database \"{:?}\" already opened.", dbfile,
|
||||||
dbfile.as_ref(),
|
|
||||||
);
|
);
|
||||||
return Err(Error::SqlAlreadyOpen.into());
|
bail!("SQL database is already opened.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open write pool
|
*self.pool.write().await = Some(Self::new_pool(dbfile, readonly)?);
|
||||||
if !readonly {
|
|
||||||
*self.writer.write().await = Some(Self::new_writer_pool(&dbfile).await?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open read pool
|
|
||||||
*self.reader.write().await = Some(Self::new_reader_pool(&dbfile, readonly).await?);
|
|
||||||
|
|
||||||
if !readonly {
|
if !readonly {
|
||||||
|
{
|
||||||
|
let conn = self.get_conn().await?;
|
||||||
|
// journal_mode is persisted, it is sufficient to change it only for one handle.
|
||||||
|
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
|
||||||
|
|
||||||
|
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
|
||||||
|
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
// (1) update low-level database structure.
|
// (1) update low-level database structure.
|
||||||
// this should be done before updates that use high-level objects that
|
// this should be done before updates that use high-level objects that
|
||||||
// rely themselves on the low-level structure.
|
// rely themselves on the low-level structure.
|
||||||
|
|
||||||
let (recalc_fingerprints, update_icons, disable_server_delete) =
|
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||||
migrations::run(context, self).await?;
|
migrations::run(context, self).await?;
|
||||||
|
|
||||||
// (2) updates that require high-level objects
|
// (2) updates that require high-level objects
|
||||||
@@ -175,13 +142,19 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c
|
|||||||
|
|
||||||
if recalc_fingerprints {
|
if recalc_fingerprints {
|
||||||
info!(context, "[migration] recalc fingerprints");
|
info!(context, "[migration] recalc fingerprints");
|
||||||
let mut rows = self
|
let addrs = self
|
||||||
.fetch(sqlx::query("SELECT addr FROM acpeerstates;"))
|
.query_map(
|
||||||
|
"SELECT addr FROM acpeerstates;",
|
||||||
|
paramsv![],
|
||||||
|
|row| row.get::<_, String>(0),
|
||||||
|
|addrs| {
|
||||||
|
addrs
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
},
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
for addr in &addrs {
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
let addr = row.try_get(0)?;
|
|
||||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||||
peerstate.recalc_fingerprint();
|
peerstate.recalc_fingerprint();
|
||||||
peerstate.save_to_db(self, false).await?;
|
peerstate.save_to_db(self, false).await?;
|
||||||
@@ -206,210 +179,206 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recode_avatar {
|
||||||
|
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
||||||
|
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
|
||||||
|
match blob.recode_to_avatar_size(context).await {
|
||||||
|
Ok(()) => {
|
||||||
|
context
|
||||||
|
.set_config(Config::Selfavatar, Some(&avatar))
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||||
|
context.set_config(Config::Selfavatar, None).await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(context, "Opened {:?}.", dbfile.as_ref());
|
info!(context, "Opened {:?}.", dbfile);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the given query, returning the number of affected rows.
|
/// Execute the given query, returning the number of affected rows.
|
||||||
pub async fn execute<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<u64>
|
pub async fn execute(
|
||||||
where
|
&self,
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
query: impl AsRef<str>,
|
||||||
{
|
params: impl rusqlite::Params,
|
||||||
let lock = self.writer.read().await;
|
) -> Result<usize> {
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
let conn = self.get_conn().await?;
|
||||||
|
let res = conn.execute(query.as_ref(), params)?;
|
||||||
let rows = pool.execute(query).await?;
|
Ok(res)
|
||||||
Ok(rows.rows_affected())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes the given query, returning the last inserted row ID.
|
/// Executes the given query, returning the last inserted row ID.
|
||||||
pub async fn insert<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<i64>
|
pub async fn insert(
|
||||||
where
|
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
|
||||||
{
|
|
||||||
let lock = self.writer.read().await;
|
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
let rows = pool.execute(query).await?;
|
|
||||||
Ok(rows.last_insert_rowid())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute many queries.
|
|
||||||
pub async fn execute_many<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<()>
|
|
||||||
where
|
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
|
||||||
{
|
|
||||||
let lock = self.writer.read().await;
|
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
pool.execute_many(query)
|
|
||||||
.collect::<sqlx::Result<Vec<_>>>()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch the given query.
|
|
||||||
pub async fn fetch<'q, E>(
|
|
||||||
&self,
|
&self,
|
||||||
query: Query<'q, Sqlite, E>,
|
query: impl AsRef<str>,
|
||||||
) -> Result<impl Stream<Item = sqlx::Result<<Sqlite as sqlx::Database>::Row>> + Send + 'q>
|
params: impl rusqlite::Params,
|
||||||
where
|
) -> anyhow::Result<usize> {
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
let conn = self.get_conn().await?;
|
||||||
{
|
conn.execute(query.as_ref(), params)?;
|
||||||
let lock = self.reader.read().await;
|
Ok(usize::try_from(conn.last_insert_rowid())?)
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
let rows = pool.fetch(query);
|
|
||||||
Ok(rows)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch exactly one row, errors if no row is found.
|
/// Prepares and executes the statement and maps a function over the resulting rows.
|
||||||
pub async fn fetch_one<'q, E>(
|
/// Then executes the second function over the returned iterator and returns the
|
||||||
|
/// result of that function.
|
||||||
|
pub async fn query_map<T, F, G, H>(
|
||||||
&self,
|
&self,
|
||||||
query: Query<'q, Sqlite, E>,
|
sql: impl AsRef<str>,
|
||||||
) -> Result<<Sqlite as sqlx::Database>::Row>
|
params: impl rusqlite::Params,
|
||||||
|
f: F,
|
||||||
|
mut g: G,
|
||||||
|
) -> Result<H>
|
||||||
where
|
where
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
F: FnMut(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
|
G: FnMut(rusqlite::MappedRows<F>) -> Result<H>,
|
||||||
{
|
{
|
||||||
let lock = self.reader.read().await;
|
let sql = sql.as_ref();
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
let row = pool.fetch_one(query).await?;
|
let conn = self.get_conn().await?;
|
||||||
Ok(row)
|
let mut stmt = conn.prepare(sql)?;
|
||||||
|
let res = stmt.query_map(params, f)?;
|
||||||
|
g(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches at most one row.
|
pub async fn get_conn(
|
||||||
pub async fn fetch_optional<'e, 'q, E>(
|
|
||||||
&self,
|
&self,
|
||||||
query: Query<'q, Sqlite, E>,
|
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
|
||||||
) -> Result<Option<<Sqlite as sqlx::Database>::Row>>
|
let lock = self.pool.read().await;
|
||||||
where
|
let pool = lock
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
.as_ref()
|
||||||
{
|
.ok_or_else(|| format_err!("No SQL connection"))?;
|
||||||
let lock = self.reader.read().await;
|
let conn = pool.get()?;
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
let row = pool.fetch_optional(query).await?;
|
Ok(conn)
|
||||||
Ok(row)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
|
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
|
||||||
pub async fn count<'e, 'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<usize>
|
pub async fn count(
|
||||||
where
|
&self,
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
query: impl AsRef<str>,
|
||||||
{
|
params: impl rusqlite::Params,
|
||||||
use std::convert::TryFrom;
|
) -> anyhow::Result<usize> {
|
||||||
|
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
|
||||||
let row = self.fetch_one(query).await?;
|
Ok(usize::try_from(count)?)
|
||||||
let count: i64 = row.try_get(0)?;
|
|
||||||
|
|
||||||
Ok(usize::try_from(count).map_err::<anyhow::Error, _>(Into::into)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for executing `SELECT COUNT` statements only. Returns `true`, if the count is at least
|
/// Used for executing `SELECT COUNT` statements only. Returns `true`, if the count is at least
|
||||||
/// one, `false` otherwise.
|
/// one, `false` otherwise.
|
||||||
pub async fn exists<'e, 'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<bool>
|
pub async fn exists(&self, sql: &str, params: impl rusqlite::Params) -> Result<bool> {
|
||||||
where
|
let count = self.count(sql, params).await?;
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
|
||||||
{
|
|
||||||
let count = self.count(query).await?;
|
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a query which is expected to return one row.
|
||||||
|
pub async fn query_row<T, F>(
|
||||||
|
&self,
|
||||||
|
query: impl AsRef<str>,
|
||||||
|
params: impl rusqlite::Params,
|
||||||
|
f: F,
|
||||||
|
) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
|
{
|
||||||
|
let conn = self.get_conn().await?;
|
||||||
|
let res = conn.query_row(query.as_ref(), params, f)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute the function inside a transaction.
|
/// Execute the function inside a transaction.
|
||||||
///
|
///
|
||||||
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
||||||
/// error, the transaction will be committed.
|
/// error, the transaction will be committed.
|
||||||
pub async fn transaction<F, R>(&self, callback: F) -> Result<R>
|
pub async fn transaction<G, H>(&self, callback: G) -> anyhow::Result<H>
|
||||||
where
|
where
|
||||||
F: for<'c> FnOnce(
|
H: Send + 'static,
|
||||||
&'c mut sqlx::Transaction<'_, Sqlite>,
|
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result<H>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<R>> + 'c + Send>>
|
|
||||||
+ 'static
|
|
||||||
+ Send
|
|
||||||
+ Sync,
|
|
||||||
R: Send,
|
|
||||||
{
|
{
|
||||||
let lock = self.writer.read().await;
|
let mut conn = self.get_conn().await?;
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
let mut transaction = conn.transaction()?;
|
||||||
|
let ret = callback(&mut transaction);
|
||||||
let mut transaction = pool.begin().await?;
|
|
||||||
let ret = callback(&mut transaction).await;
|
|
||||||
|
|
||||||
match ret {
|
match ret {
|
||||||
Ok(ret) => {
|
Ok(ret) => {
|
||||||
transaction.commit().await?;
|
transaction.commit()?;
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
transaction.rollback().await?;
|
transaction.rollback()?;
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query the database if the requested table already exists.
|
/// Query the database if the requested table already exists.
|
||||||
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
|
pub async fn table_exists(&self, name: &str) -> anyhow::Result<bool> {
|
||||||
let q = format!("PRAGMA table_info(\"{}\")", name.as_ref());
|
let conn = self.get_conn().await?;
|
||||||
|
let mut exists = false;
|
||||||
|
conn.pragma(None, "table_info", &name.to_string(), |_row| {
|
||||||
|
// will only be executed if the info was found
|
||||||
|
exists = true;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
let lock = self.reader.read().await;
|
Ok(exists)
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
|
||||||
|
|
||||||
let mut rows = pool.fetch(sqlx::query(&q));
|
|
||||||
if let Some(first_row) = rows.next().await {
|
|
||||||
Ok(first_row.is_ok())
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a column exists in a given table.
|
/// Check if a column exists in a given table.
|
||||||
pub async fn col_exists(
|
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> anyhow::Result<bool> {
|
||||||
&self,
|
let conn = self.get_conn().await?;
|
||||||
table_name: impl AsRef<str>,
|
let mut exists = false;
|
||||||
col_name: impl AsRef<str>,
|
// `PRAGMA table_info` returns one row per column,
|
||||||
) -> Result<bool> {
|
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
|
||||||
let q = format!("PRAGMA table_info(\"{}\")", table_name.as_ref());
|
conn.pragma(None, "table_info", &table_name.to_string(), |row| {
|
||||||
let lock = self.reader.read().await;
|
let curr_name: String = row.get(1)?;
|
||||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
if col_name == curr_name {
|
||||||
|
exists = true;
|
||||||
let mut rows = pool.fetch(sqlx::query(&q));
|
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
|
|
||||||
// `PRAGMA table_info` returns one row per column,
|
|
||||||
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
|
|
||||||
|
|
||||||
let curr_name: &str = row.try_get(1)?;
|
|
||||||
if col_name.as_ref() == curr_name {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
}
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(false)
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a query which is expected to return zero or one row.
|
||||||
|
pub async fn query_row_optional<T, F>(
|
||||||
|
&self,
|
||||||
|
sql: impl AsRef<str>,
|
||||||
|
params: impl rusqlite::Params,
|
||||||
|
f: F,
|
||||||
|
) -> anyhow::Result<Option<T>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
|
{
|
||||||
|
let conn = self.get_conn().await?;
|
||||||
|
let res = match conn.query_row(sql.as_ref(), params, f) {
|
||||||
|
Ok(res) => Ok(Some(res)),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(rusqlite::Error::InvalidColumnType(_, _, rusqlite::types::Type::Null)) => Ok(None),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}?;
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a query which is expected to return one row and one
|
/// Executes a query which is expected to return one row and one
|
||||||
/// column. If the query does not return a value or returns SQL
|
/// column. If the query does not return a value or returns SQL
|
||||||
/// `NULL`, returns `Ok(None)`.
|
/// `NULL`, returns `Ok(None)`.
|
||||||
pub async fn query_get_value<'e, 'q, E, T>(
|
pub async fn query_get_value<T>(
|
||||||
&self,
|
&self,
|
||||||
query: Query<'q, Sqlite, E>,
|
query: &str,
|
||||||
) -> Result<Option<T>>
|
params: impl rusqlite::Params,
|
||||||
|
) -> anyhow::Result<Option<T>>
|
||||||
where
|
where
|
||||||
E: 'q + IntoArguments<'q, Sqlite>,
|
T: rusqlite::types::FromSql,
|
||||||
T: for<'r> sqlx::Decode<'r, Sqlite> + sqlx::Type<Sqlite>,
|
|
||||||
{
|
{
|
||||||
let res = self
|
self.query_row_optional(query, params, |row| row.get::<_, T>(0))
|
||||||
.fetch_optional(query)
|
.await
|
||||||
.await?
|
|
||||||
.map(|row| row.get::<T, _>(0));
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set private configuration options.
|
/// Set private configuration options.
|
||||||
@@ -417,33 +386,30 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c
|
|||||||
/// Setting `None` deletes the value. On failure an error message
|
/// Setting `None` deletes the value. On failure an error message
|
||||||
/// will already have been logged.
|
/// will already have been logged.
|
||||||
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
|
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
|
||||||
if !self.is_open().await {
|
|
||||||
return Err(Error::SqlNoConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = key.as_ref();
|
let key = key.as_ref();
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
let exists = self
|
let exists = self
|
||||||
.exists(sqlx::query("SELECT COUNT(*) FROM config WHERE keyname=?;").bind(key))
|
.exists(
|
||||||
|
"SELECT COUNT(*) FROM config WHERE keyname=?;",
|
||||||
|
paramsv![key],
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
self.execute(
|
self.execute(
|
||||||
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
|
"UPDATE config SET value=? WHERE keyname=?;",
|
||||||
.bind(value)
|
paramsv![value, key],
|
||||||
.bind(key),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
self.execute(
|
self.execute(
|
||||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||||
.bind(key)
|
paramsv![key, value],
|
||||||
.bind(value),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.execute(sqlx::query("DELETE FROM config WHERE keyname=?;").bind(key))
|
self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key])
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,12 +418,10 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c
|
|||||||
|
|
||||||
/// Get configuration options from the database.
|
/// Get configuration options from the database.
|
||||||
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
|
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
|
||||||
if !self.is_open().await || key.as_ref().is_empty() {
|
|
||||||
return Err(Error::SqlNoConnection);
|
|
||||||
}
|
|
||||||
let value = self
|
let value = self
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT value FROM config WHERE keyname=?;").bind(key.as_ref()),
|
"SELECT value FROM config WHERE keyname=?;",
|
||||||
|
paramsv![key.as_ref()],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
|
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
|
||||||
@@ -539,14 +503,21 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut rows = context
|
context
|
||||||
.sql
|
.sql
|
||||||
.fetch(sqlx::query("SELECT value FROM config;"))
|
.query_map(
|
||||||
.await?;
|
"SELECT value FROM config;",
|
||||||
while let Some(row) = rows.next().await {
|
paramsv![],
|
||||||
let row: String = row?.try_get(0)?;
|
|row| row.get::<_, String>(0),
|
||||||
maybe_add_file(&mut files_in_use, row);
|
|rows| {
|
||||||
}
|
for row in rows {
|
||||||
|
maybe_add_file(&mut files_in_use, row?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("housekeeping: failed to SELECT value FROM config")?;
|
||||||
|
|
||||||
info!(context, "{} files in use.", files_in_use.len(),);
|
info!(context, "{} files in use.", files_in_use.len(),);
|
||||||
/* go through directory and delete unused files */
|
/* go through directory and delete unused files */
|
||||||
@@ -665,14 +636,22 @@ async fn maybe_add_from_param(
|
|||||||
query: &str,
|
query: &str,
|
||||||
param_id: Param,
|
param_id: Param,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut rows = sql.fetch(sqlx::query(query)).await?;
|
sql.query_map(
|
||||||
while let Some(row) = rows.next().await {
|
query,
|
||||||
let row: String = row?.try_get(0)?;
|
paramsv![],
|
||||||
let param: Params = row.parse().unwrap_or_default();
|
|row| row.get::<_, String>(0),
|
||||||
if let Some(file) = param.get(param_id) {
|
|rows| {
|
||||||
maybe_add_file(files_in_use, file);
|
for row in rows {
|
||||||
}
|
let param: Params = row?.parse().unwrap_or_default();
|
||||||
}
|
if let Some(file) = param.get(param_id) {
|
||||||
|
maybe_add_file(files_in_use, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context(format!("housekeeping: failed to add_from_param {}", query))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -681,25 +660,15 @@ async fn maybe_add_from_param(
|
|||||||
/// have a server UID.
|
/// have a server UID.
|
||||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||||
sql.execute(
|
sql.execute(
|
||||||
sqlx::query(
|
"DELETE FROM msgs \
|
||||||
"DELETE FROM msgs \
|
|
||||||
WHERE (chat_id = ? OR hidden) \
|
WHERE (chat_id = ? OR hidden) \
|
||||||
AND server_uid = 0",
|
AND server_uid = 0",
|
||||||
)
|
paramsv![DC_CHAT_ID_TRASH],
|
||||||
.bind(DC_CHAT_ID_TRASH),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SQLite version as a string; e.g., `"3.16.2"` for version 3.16.2.
|
|
||||||
pub fn version() -> &'static str {
|
|
||||||
#[allow(unsafe_code)]
|
|
||||||
let cstr = unsafe { std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion()) };
|
|
||||||
cstr.to_str()
|
|
||||||
.expect("SQLite version string is not valid UTF8 ?!")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use async_std::fs::File;
|
use async_std::fs::File;
|
||||||
@@ -755,7 +724,7 @@ mod test {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
let avatar_src = t.dir.path().join("avatar.png");
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
File::create(&avatar_src)
|
File::create(&avatar_src)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -784,7 +753,7 @@ mod test {
|
|||||||
|
|
||||||
t.sql.close().await;
|
t.sql.close().await;
|
||||||
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
|
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
|
||||||
t.sql.open(&t, &t.get_dbfile(), false).await.unwrap();
|
t.sql.open(&t, t.get_dbfile(), false).await.unwrap();
|
||||||
|
|
||||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||||
@@ -815,20 +784,19 @@ mod test {
|
|||||||
let sql = Sql::new();
|
let sql = Sql::new();
|
||||||
|
|
||||||
// Create database with all the tables.
|
// Create database with all the tables.
|
||||||
sql.open(&t, &dbfile, false).await.unwrap();
|
sql.open(&t, dbfile.as_ref(), false).await.unwrap();
|
||||||
sql.close().await;
|
sql.close().await;
|
||||||
|
|
||||||
// Reopen the database
|
// Reopen the database
|
||||||
sql.open(&t, &dbfile, false).await?;
|
sql.open(&t, dbfile.as_ref(), false).await?;
|
||||||
sql.execute(
|
sql.execute(
|
||||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||||
.bind("foo")
|
paramsv!("foo", "bar"),
|
||||||
.bind("bar"),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let value: Option<String> = sql
|
let value: Option<String> = sql
|
||||||
.query_get_value(sqlx::query("SELECT value FROM config WHERE keyname=?;").bind("foo"))
|
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv!("foo"))
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(value.unwrap(), "bar");
|
assert_eq!(value.unwrap(), "bar");
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Sqlx: {0:?}")]
|
|
||||||
Sqlx(#[from] sqlx::Error),
|
|
||||||
#[error("Sqlite: Connection closed")]
|
|
||||||
SqlNoConnection,
|
|
||||||
#[error("Sqlite: Already open")]
|
|
||||||
SqlAlreadyOpen,
|
|
||||||
#[error("Sqlite: Failed to open")]
|
|
||||||
SqlFailedToOpen,
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
// #[error("{0:?}")]
|
|
||||||
// BlobError(#[from] crate::blob::BlobError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Other(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
@@ -1,40 +1,33 @@
|
|||||||
use async_std::prelude::*;
|
use anyhow::Result;
|
||||||
|
|
||||||
use super::{Result, Sql};
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::ShowEmails;
|
use crate::constants::ShowEmails;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::EmailAddress;
|
use crate::dc_tools::EmailAddress;
|
||||||
use crate::imap;
|
use crate::imap;
|
||||||
use crate::provider::get_provider_by_domain;
|
use crate::provider::get_provider_by_domain;
|
||||||
|
use crate::sql::Sql;
|
||||||
|
|
||||||
const DBVERSION: i32 = 68;
|
const DBVERSION: i32 = 68;
|
||||||
const VERSION_CFG: &str = "dbversion";
|
const VERSION_CFG: &str = "dbversion";
|
||||||
const TABLES: &str = include_str!("./tables.sql");
|
const TABLES: &str = include_str!("./tables.sql");
|
||||||
|
|
||||||
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
|
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool)> {
|
||||||
let mut recalc_fingerprints = false;
|
let mut recalc_fingerprints = false;
|
||||||
let mut exists_before_update = false;
|
let mut exists_before_update = false;
|
||||||
let mut dbversion_before_update = DBVERSION;
|
let mut dbversion_before_update = DBVERSION;
|
||||||
|
|
||||||
if !sql.table_exists("config").await? {
|
if !sql.table_exists("config").await? {
|
||||||
info!(context, "First time init: creating tables",);
|
info!(context, "First time init: creating tables",);
|
||||||
sql.transaction(move |conn| {
|
sql.transaction(move |transaction| {
|
||||||
Box::pin(async move {
|
transaction.execute_batch(TABLES)?;
|
||||||
sqlx::query(TABLES)
|
|
||||||
.execute_many(&mut *conn)
|
|
||||||
.await
|
|
||||||
.collect::<std::result::Result<Vec<_>, _>>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// set raw config inside the transaction
|
// set raw config inside the transaction
|
||||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
transaction.execute(
|
||||||
.bind(VERSION_CFG)
|
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||||
.bind(format!("{}", dbversion_before_update))
|
paramsv![VERSION_CFG, format!("{}", dbversion_before_update)],
|
||||||
.execute(&mut *conn)
|
)?;
|
||||||
.await?;
|
Ok(())
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
@@ -48,6 +41,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
|
|||||||
let dbversion = dbversion_before_update;
|
let dbversion = dbversion_before_update;
|
||||||
let mut update_icons = !exists_before_update;
|
let mut update_icons = !exists_before_update;
|
||||||
let mut disable_server_delete = false;
|
let mut disable_server_delete = false;
|
||||||
|
let mut recode_avatar = false;
|
||||||
|
|
||||||
if dbversion < 1 {
|
if dbversion < 1 {
|
||||||
info!(context, "[migration] v1");
|
info!(context, "[migration] v1");
|
||||||
@@ -417,9 +411,10 @@ ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
|
|||||||
if dbversion < 73 {
|
if dbversion < 73 {
|
||||||
use Config::*;
|
use Config::*;
|
||||||
info!(context, "[migration] v73");
|
info!(context, "[migration] v73");
|
||||||
sql.execute(sqlx::query(
|
sql.execute(
|
||||||
r#"
|
r#"
|
||||||
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#),
|
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
|
||||||
|
paramsv![]
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for c in &[
|
for c in &[
|
||||||
@@ -468,8 +463,17 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
|
|||||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
|
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
if dbversion < 77 {
|
||||||
|
info!(context, "[migration] v77");
|
||||||
|
recode_avatar = true;
|
||||||
|
}
|
||||||
|
|
||||||
Ok((recalc_fingerprints, update_icons, disable_server_delete))
|
Ok((
|
||||||
|
recalc_fingerprints,
|
||||||
|
update_icons,
|
||||||
|
disable_server_delete,
|
||||||
|
recode_avatar,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sql {
|
impl Sql {
|
||||||
@@ -479,24 +483,16 @@ impl Sql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> {
|
async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> {
|
||||||
let query = sqlx::query(query);
|
self.transaction(move |transaction| {
|
||||||
self.transaction(move |conn| {
|
transaction.execute_batch(query)?;
|
||||||
Box::pin(async move {
|
|
||||||
query
|
|
||||||
.execute_many(&mut *conn)
|
|
||||||
.await
|
|
||||||
.collect::<std::result::Result<Vec<_>, _>>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// set raw config inside the transaction
|
// set raw config inside the transaction
|
||||||
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
|
transaction.execute(
|
||||||
.bind(format!("{}", version))
|
"UPDATE config SET value=? WHERE keyname=?;",
|
||||||
.bind(VERSION_CFG)
|
paramsv![format!("{}", version), VERSION_CFG],
|
||||||
.execute(&mut *conn)
|
)?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use strum::EnumProperty;
|
|||||||
use strum_macros::EnumProperty;
|
use strum_macros::EnumProperty;
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat;
|
use crate::chat::{self, ChatId, ProtectionStatus};
|
||||||
use crate::chat::ProtectionStatus;
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||||
use crate::contact::{Contact, Origin};
|
use crate::contact::{Contact, Origin};
|
||||||
@@ -262,6 +261,9 @@ pub enum StockMessage {
|
|||||||
|
|
||||||
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks."))]
|
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks."))]
|
||||||
MsgEphemeralTimerWeeks = 96,
|
MsgEphemeralTimerWeeks = 96,
|
||||||
|
|
||||||
|
#[strum(props(fallback = "Forwarded"))]
|
||||||
|
Forwarded = 97,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StockMessage {
|
impl StockMessage {
|
||||||
@@ -856,6 +858,11 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stock string: `Forwarded`.
|
||||||
|
pub(crate) async fn forwarded(context: &Context) -> String {
|
||||||
|
translated(context, StockMessage::Forwarded).await
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Set the stock string for the [StockMessage].
|
/// Set the stock string for the [StockMessage].
|
||||||
///
|
///
|
||||||
@@ -908,7 +915,7 @@ impl Context {
|
|||||||
self.sql
|
self.sql
|
||||||
.set_raw_config_bool("self-chat-added", true)
|
.set_raw_config_bool("self-chat-added", true)
|
||||||
.await?;
|
.await?;
|
||||||
chat::create_by_contact_id(self, DC_CONTACT_ID_SELF).await?;
|
ChatId::create_for_contact(self, DC_CONTACT_ID_SELF).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add welcome-messages. by the label, this is done only once,
|
// add welcome-messages. by the label, this is done only once,
|
||||||
@@ -918,7 +925,7 @@ impl Context {
|
|||||||
chat::add_device_msg(self, Some("core-about-device-chat"), Some(&mut msg)).await?;
|
chat::add_device_msg(self, Some("core-about-device-chat"), Some(&mut msg)).await?;
|
||||||
|
|
||||||
let image = include_bytes!("../assets/welcome-image.jpg");
|
let image = include_bytes!("../assets/welcome-image.jpg");
|
||||||
let blob = BlobObject::create(self, "welcome-image.jpg".to_string(), image).await?;
|
let blob = BlobObject::create(self, "welcome-image.jpg", image).await?;
|
||||||
let mut msg = Message::new(Viewtype::Image);
|
let mut msg = Message::new(Viewtype::Image);
|
||||||
msg.param.set(Param::File, blob.as_name());
|
msg.param.set(Param::File, blob.as_name());
|
||||||
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use async_std::{channel, pin::Pin};
|
|||||||
use async_std::{future::Future, task};
|
use async_std::{future::Future, task};
|
||||||
use chat::ChatItem;
|
use chat::ChatItem;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use sqlx::Row;
|
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
use crate::chat::{self, Chat, ChatId};
|
use crate::chat::{self, Chat, ChatId};
|
||||||
@@ -228,25 +227,22 @@ impl TestContext {
|
|||||||
let row = self
|
let row = self
|
||||||
.ctx
|
.ctx
|
||||||
.sql
|
.sql
|
||||||
.fetch_one(
|
.query_row(
|
||||||
sqlx::query(
|
r#"
|
||||||
r#"
|
|
||||||
SELECT id, foreign_id, param
|
SELECT id, foreign_id, param
|
||||||
FROM jobs
|
FROM jobs
|
||||||
WHERE action=?
|
WHERE action=?
|
||||||
ORDER BY desired_timestamp DESC;
|
ORDER BY desired_timestamp DESC;
|
||||||
"#,
|
"#,
|
||||||
)
|
paramsv![Action::SendMsgToSmtp],
|
||||||
.bind(Action::SendMsgToSmtp),
|
|row| {
|
||||||
|
let id: u32 = row.get(0)?;
|
||||||
|
let foreign_id: u32 = row.get(1)?;
|
||||||
|
let param: String = row.get(2)?;
|
||||||
|
Ok((id, foreign_id, param))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.and_then(|row| {
|
|
||||||
let id: u32 = row.try_get(0)?;
|
|
||||||
let foreign_id: u32 = row.try_get(1)?;
|
|
||||||
let param: String = row.try_get(2)?;
|
|
||||||
Ok((id, foreign_id, param))
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(row) = row {
|
if let Ok(row) = row {
|
||||||
break row;
|
break row;
|
||||||
}
|
}
|
||||||
@@ -266,7 +262,7 @@ impl TestContext {
|
|||||||
.to_abs_path();
|
.to_abs_path();
|
||||||
self.ctx
|
self.ctx
|
||||||
.sql
|
.sql
|
||||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(rowid))
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
||||||
.await
|
.await
|
||||||
.expect("failed to remove job");
|
.expect("failed to remove job");
|
||||||
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
||||||
@@ -342,13 +338,13 @@ impl TestContext {
|
|||||||
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
||||||
let (contact_id, _modified) = Contact::add_or_lookup(
|
let (contact_id, _modified) = Contact::add_or_lookup(
|
||||||
self,
|
self,
|
||||||
other
|
&other
|
||||||
.ctx
|
.ctx
|
||||||
.get_config(Config::Displayname)
|
.get_config(Config::Displayname)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
other
|
&other
|
||||||
.ctx
|
.ctx
|
||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
.await
|
.await
|
||||||
@@ -359,7 +355,7 @@ impl TestContext {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let chat_id = chat::create_by_contact_id(self, contact_id).await.unwrap();
|
let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap();
|
||||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,13 +367,13 @@ impl TestContext {
|
|||||||
let contact = Contact::create(self, name, addr)
|
let contact = Contact::create(self, name, addr)
|
||||||
.await
|
.await
|
||||||
.expect("failed to create contact");
|
.expect("failed to create contact");
|
||||||
let chat_id = chat::create_by_contact_id(self, contact).await.unwrap();
|
let chat_id = ChatId::create_for_contact(self, contact).await.unwrap();
|
||||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the "self" chat.
|
/// Retrieves the "self" chat.
|
||||||
pub async fn get_self_chat(&self) -> Chat {
|
pub async fn get_self_chat(&self) -> Chat {
|
||||||
let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF)
|
let chat_id = ChatId::create_for_contact(self, DC_CONTACT_ID_SELF)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||||
|
|||||||
40
src/token.rs
40
src/token.rs
@@ -4,12 +4,17 @@
|
|||||||
//!
|
//!
|
||||||
//! Tokens are used in countermitm verification protocols.
|
//! Tokens are used in countermitm verification protocols.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
|
|
||||||
use crate::chat::ChatId;
|
use crate::chat::ChatId;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::{dc_create_id, time};
|
use crate::dc_tools::{dc_create_id, time};
|
||||||
|
|
||||||
/// Token namespace
|
/// Token namespace
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
#[derive(
|
||||||
|
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||||
|
)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum Namespace {
|
pub enum Namespace {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -32,25 +37,16 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: Option<Ch
|
|||||||
Some(foreign_id) => context
|
Some(foreign_id) => context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);"
|
paramsv![namespace, foreign_id, token, time()],
|
||||||
)
|
|
||||||
.bind(namespace)
|
|
||||||
.bind(foreign_id)
|
|
||||||
.bind(&token)
|
|
||||||
.bind(time()),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.ok(),
|
.ok(),
|
||||||
None => context
|
None => context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);",
|
||||||
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);"
|
paramsv![namespace, token, time()],
|
||||||
)
|
|
||||||
.bind(namespace)
|
|
||||||
.bind(&token)
|
|
||||||
.bind(time()),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.ok(),
|
.ok(),
|
||||||
@@ -63,15 +59,14 @@ pub async fn lookup(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
namespace: Namespace,
|
namespace: Namespace,
|
||||||
chat: Option<ChatId>,
|
chat: Option<ChatId>,
|
||||||
) -> crate::sql::Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
let token = match chat {
|
let token = match chat {
|
||||||
Some(chat_id) => {
|
Some(chat_id) => {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;")
|
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
|
||||||
.bind(namespace)
|
paramsv![namespace, chat_id],
|
||||||
.bind(chat_id),
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
@@ -80,8 +75,8 @@ pub async fn lookup(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_get_value(
|
||||||
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;")
|
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;",
|
||||||
.bind(namespace),
|
paramsv![namespace],
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
@@ -105,9 +100,8 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
sqlx::query("SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;")
|
"SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;",
|
||||||
.bind(namespace)
|
paramsv![namespace, token],
|
||||||
.bind(token),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|||||||
Reference in New Issue
Block a user