mirror of
https://github.com/chatmail/core.git
synced 2026-04-04 22:42:11 +03:00
Compare commits
51 Commits
alias-supp
...
1.52.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc6b02f037 | ||
|
|
e13bb8fbd4 | ||
|
|
eaca4446aa | ||
|
|
f3fb26c066 | ||
|
|
1a1416e446 | ||
|
|
0afc07f6e7 | ||
|
|
dda3c605c6 | ||
|
|
d4e065ee84 | ||
|
|
bc222af661 | ||
|
|
f6136f0ecc | ||
|
|
b2517d3060 | ||
|
|
244260a978 | ||
|
|
f17320a9cb | ||
|
|
d1237c9f8d | ||
|
|
b9beaee7d4 | ||
|
|
258856c23a | ||
|
|
72ddd33adf | ||
|
|
1cd53aafff | ||
|
|
4d2ac5a3a2 | ||
|
|
146db48c35 | ||
|
|
9529d76d82 | ||
|
|
b5f2752e41 | ||
|
|
ce4675e9f7 | ||
|
|
f0bd129636 | ||
|
|
dfe3cabb14 | ||
|
|
09735b808e | ||
|
|
37f68459f6 | ||
|
|
3707471266 | ||
|
|
5394327bf6 | ||
|
|
df277b374d | ||
|
|
53dba3c1ba | ||
|
|
6540ee60e5 | ||
|
|
66b5084a1d | ||
|
|
f76aaf3205 | ||
|
|
179a2a50e6 | ||
|
|
720135a915 | ||
|
|
6bb5721f29 | ||
|
|
4dedc2d8ce | ||
|
|
ede9bdc018 | ||
|
|
11823d3b45 | ||
|
|
734ea8ab1b | ||
|
|
7017a050cb | ||
|
|
96e57e7ef3 | ||
|
|
02bc334af5 | ||
|
|
c8fea9c577 | ||
|
|
cdc1063d83 | ||
|
|
704a902cc5 | ||
|
|
36aef6499d | ||
|
|
4ba9c2fafa | ||
|
|
0de8b6a7e5 | ||
|
|
04f816be31 |
@@ -8,107 +8,7 @@ executors:
|
||||
docker:
|
||||
- image: hrektts/doxygen
|
||||
|
||||
|
||||
restore-workspace: &restore-workspace
|
||||
attach_workspace:
|
||||
at: /mnt
|
||||
|
||||
restore-cache: &restore-cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
- repo-source-{{ .Branch }}-{{ .Revision }}
|
||||
|
||||
commands:
|
||||
test_target:
|
||||
parameters:
|
||||
target:
|
||||
type: string
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Test (<< parameters.target >>)
|
||||
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
|
||||
no_output_timeout: 15m
|
||||
|
||||
jobs:
|
||||
cargo_fetch:
|
||||
executor: default
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
- run: rustup install $(cat rust-toolchain)
|
||||
- run: rustup default $(cat rust-toolchain)
|
||||
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
|
||||
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
|
||||
- run: cargo fetch
|
||||
- run: rustc +stable --version
|
||||
- run: rustc +$(cat rust-toolchain) --version
|
||||
# make sure this git repo doesn't grow too big
|
||||
- run: git gc
|
||||
- persist_to_workspace:
|
||||
root: /mnt
|
||||
paths:
|
||||
- crate
|
||||
- save_cache:
|
||||
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- "~/.cargo"
|
||||
- "~/.rustup"
|
||||
|
||||
rustfmt:
|
||||
executor: default
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Run cargo fmt
|
||||
command: cargo fmt --all -- --check
|
||||
|
||||
test_macos:
|
||||
macos:
|
||||
xcode: "10.0.0"
|
||||
working_directory: ~/crate
|
||||
steps:
|
||||
- run:
|
||||
name: Configure environment variables
|
||||
command: |
|
||||
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
|
||||
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Rust
|
||||
command: |
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- run: rustup install $(cat rust-toolchain)
|
||||
- run: rustup default $(cat rust-toolchain)
|
||||
- run: cargo fetch
|
||||
- run:
|
||||
name: Test
|
||||
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
|
||||
|
||||
test_x86_64-unknown-linux-gnu:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
|
||||
test_i686-unknown-linux-gnu:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "i686-unknown-linux-gnu"
|
||||
|
||||
test_aarch64-linux-android:
|
||||
executor: default
|
||||
steps:
|
||||
- test_target:
|
||||
target: "aarch64-linux-android"
|
||||
|
||||
|
||||
build_doxygen:
|
||||
executor: doxygen
|
||||
steps:
|
||||
@@ -135,18 +35,6 @@ jobs:
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
remote_tests_rust:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: ci_scripts/remote_tests_rust.sh
|
||||
|
||||
remote_tests_python:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: ci_scripts/remote_tests_python.sh
|
||||
|
||||
upload_docs_wheels:
|
||||
machine: true
|
||||
steps:
|
||||
@@ -156,39 +44,15 @@ jobs:
|
||||
- run: ls -laR workspace
|
||||
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||
|
||||
clippy:
|
||||
executor: default
|
||||
steps:
|
||||
- *restore-workspace
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Run cargo clippy
|
||||
command: cargo clippy
|
||||
|
||||
|
||||
workflows:
|
||||
version: 2.1
|
||||
|
||||
test:
|
||||
jobs:
|
||||
# - cargo_fetch
|
||||
|
||||
- remote_tests_rust:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- remote_tests_python:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- remote_python_packaging:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
#tags:
|
||||
# only: /.*/
|
||||
|
||||
- upload_docs_wheels:
|
||||
requires:
|
||||
@@ -197,38 +61,8 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
# - rustfmt:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
# - clippy:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
# Linux Desktop 64bit
|
||||
# - test_x86_64-unknown-linux-gnu:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Linux Desktop 32bit
|
||||
# - test_i686-unknown-linux-gnu:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Android 64bit
|
||||
# - test_aarch64-linux-android:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
# Desktop Apple
|
||||
# - test_macos:
|
||||
# requires:
|
||||
# - cargo_fetch
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.50.0
|
||||
|
||||
21
.github/workflows/remote_tests.yml
vendored
Normal file
21
.github/workflows/remote_tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Remote tests
|
||||
on: [push]
|
||||
jobs:
|
||||
remote_tests_python:
|
||||
name: Remote Python tests
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: mkdir -m 700 -p ~/.ssh
|
||||
- run: touch ~/.ssh/id_ed25519
|
||||
- run: chmod 600 ~/.ssh/id_ed25519
|
||||
- run: 'echo "$SSH_KEY" | base64 -d > ~/.ssh/id_ed25519'
|
||||
shell: bash
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
- run: ci_scripts/remote_tests_python.sh
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/build
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,22 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
## UNRELEASED
|
||||
## 1.52.0
|
||||
|
||||
- breaking change: You have to call dc_stop_io()/dc_start_io() before/after EXPORT_BACKUP:
|
||||
fix race condition and db corruption when a message was received during backup #2253
|
||||
- database library changed from rusqlite to sqlx #2089 #2331 #2336 #2340
|
||||
|
||||
- save subject for messages:
|
||||
new api `dc_msg_get_subject()`,
|
||||
when quoting, use the subject of the quoted message as the new subject, instead of the
|
||||
last subject in the chat
|
||||
- add alias support: UIs should check for `dc_msg_get_override_sender_name()`
|
||||
also in single-chats now and display divergent names and avatars #2297
|
||||
|
||||
- parse blockquote-tags for better quote detection #2313
|
||||
|
||||
- ignore unknown classical emails from spam folder #2311
|
||||
|
||||
- support "Mixed Up” encryption repairing #2321
|
||||
|
||||
- fix single chat search #2344
|
||||
|
||||
- fix nightly clippy and rustc errors #2341
|
||||
|
||||
- update dependencies #2350
|
||||
|
||||
- improve ci #2342
|
||||
|
||||
- improve python bindings #2332 #2326
|
||||
|
||||
|
||||
## 1.51.0
|
||||
|
||||
- breaking change: You have to call `dc_stop_io()`/`dc_start_io()`
|
||||
before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`:
|
||||
fix race condition and db corruption
|
||||
when a message was received during backup #2253
|
||||
|
||||
- save subject for messages: new api `dc_msg_get_subject()`,
|
||||
when quoting, use the subject of the quoted message as the new subject,
|
||||
instead of the last subject in the chat #2274 #2283
|
||||
|
||||
- new apis to get full or html message,
|
||||
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151
|
||||
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 #2264 #2279
|
||||
|
||||
- new chat type and apis for the new mailing list support,
|
||||
`DC_CHAT_TYPE_MAILINGLIST`, `dc_msg_get_real_chat_id()`,
|
||||
`dc_msg_get_override_sender_name()` #1964 #2181 #2185 #2195 #2211 #2210 #2240
|
||||
#2243
|
||||
#2241 #2243 #2258 #2259 #2261 #2267 #2270 #2272 #2290
|
||||
|
||||
- new api `dc_decide_on_contact_request()`,
|
||||
deprecated `dc_create_chat_by_msg_id()` and `dc_marknoticed_contact()` #1964
|
||||
@@ -25,7 +50,7 @@
|
||||
|
||||
- new api `dc_get_chat_encrinfo()` #2186
|
||||
|
||||
- new api `dc_contact_get_status()`, returning the recent footer #2218
|
||||
- new api `dc_contact_get_status()`, returning the recent footer #2218 #2307
|
||||
|
||||
- improve contact name update rules,
|
||||
add api `dc_contact_get_auth_name()` #2206 #2212 #2225
|
||||
@@ -38,6 +63,11 @@
|
||||
|
||||
- api removed: `dc_contact_get_first_name()` #2165 #2171
|
||||
|
||||
- improve compatibility with providers changing the Message-ID
|
||||
(as Outlook.com) #2250 #2265
|
||||
|
||||
- correctly show emails that were sent to an alias and then bounced
|
||||
|
||||
- implement Consistent Color Generation (XEP-0392),
|
||||
that results in contact colors be be changed #2228 #2229 #2239
|
||||
|
||||
@@ -72,7 +102,7 @@
|
||||
|
||||
- enable strict TLS for known providers by default #2121
|
||||
|
||||
- improve and harden secure join #2154 #2161
|
||||
- improve and harden secure join #2154 #2161 #2251
|
||||
|
||||
- update `dc_get_info()` to return more information #2156
|
||||
|
||||
@@ -100,10 +130,24 @@
|
||||
|
||||
- fix parsing quoted encoded words in From: header #2193 #2204
|
||||
|
||||
- fix ci #2217 #2226
|
||||
- fix import/export race condition #2250
|
||||
|
||||
- fix: exclude muted chats from notified-list #2269 #2275
|
||||
|
||||
- fix: update uid_next if the server rewind it #2288
|
||||
|
||||
- fix: return error on fingerprint mismatch on qr-scan #2295
|
||||
|
||||
- fix ci #2217 #2226 #2244 #2245 #2249 #2277 #2286
|
||||
|
||||
- try harder on backup opening #2148
|
||||
|
||||
- trash messages more thoroughly #2273
|
||||
|
||||
- nicer logging #2284
|
||||
|
||||
- add CMakeLists.txt #2260
|
||||
|
||||
- switch to rust 1.50, update toolchains, deps #2150 #2155 #2165 #2107 #2262 #2271
|
||||
|
||||
- improve python bindings #2113 #2115 #2133 #2214
|
||||
@@ -111,7 +155,9 @@
|
||||
- improve documentation #2143 #2160 #2175 #2146
|
||||
|
||||
- refactorings #2110 #2136 #2135 #2168 #2178 #2189 #2190 #2198 #2197 #2201 #2196
|
||||
#2200 #2230
|
||||
#2200 #2230 #2262 #2203
|
||||
|
||||
- update provider-database #2299
|
||||
|
||||
|
||||
## 1.50.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project (deltachat)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
|
||||
903
Cargo.lock
generated
903
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
124
Cargo.toml
124
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.51.0-alpha.0"
|
||||
version = "1.52.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -12,83 +12,80 @@ debug = 0
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
hex = "0.4.0"
|
||||
sha-1 = "0.9.3"
|
||||
sha2 = "0.9.0"
|
||||
rand = "0.7.0"
|
||||
smallvec = "1.0.0"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.28"
|
||||
async-imap = "0.4.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-std = { version = "~1.8.0", features = ["unstable"] }
|
||||
base64 = "0.12"
|
||||
charset = "0.1"
|
||||
percent-encoding = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
indexmap = "1.3.0"
|
||||
kamadak-exif = "0.5"
|
||||
once_cell = "1.4.1"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.24", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.17.0"
|
||||
r2d2 = "0.8.5"
|
||||
strum = "0.19.0"
|
||||
strum_macros = "0.19.0"
|
||||
backtrace = "0.3.33"
|
||||
byteorder = "1.3.1"
|
||||
itertools = "0.9.0"
|
||||
quick-xml = "0.18.1"
|
||||
escaper = "0.1.0"
|
||||
bitflags = "1.1.0"
|
||||
sanitize-filename = "0.3.0"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.13.0"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
futures = "0.3.4"
|
||||
thiserror = "1.0.14"
|
||||
anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-std-resolver = "0.19.5"
|
||||
async-std = { version = "~1.8.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
rust-hsluv = "0.1.4"
|
||||
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
async-trait = "0.1.31"
|
||||
backtrace = "0.3.33"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.1.0"
|
||||
byteorder = "1.3.1"
|
||||
charset = "0.1"
|
||||
chrono = "0.4.6"
|
||||
dirs = { version = "3.0.1", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
escaper = "0.1.0"
|
||||
futures = "0.3.4"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.3.0"
|
||||
itertools = "0.10.0"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.51"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.0"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
once_cell = "1.4.1"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
quick-xml = "0.18.1"
|
||||
rand = "0.7.0"
|
||||
regex = "1.1.6"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.3"
|
||||
sha2 = "0.9.0"
|
||||
smallvec = "1.0.0"
|
||||
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] }
|
||||
# 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_macros = "0.20.1"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1.0.14"
|
||||
toml = "0.5.6"
|
||||
|
||||
url = "2.1.1"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.7.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.6.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
|
||||
futures-lite = "1.7.0"
|
||||
criterion = "0.3"
|
||||
ansi_term = "0.12.0"
|
||||
tempfile = "3.0"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
]
|
||||
|
||||
[[example]]
|
||||
@@ -112,4 +109,3 @@ internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ $ curl https://sh.rustup.rs -sSf | sh
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
|
||||
```
|
||||
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||
```
|
||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||
|
||||
@@ -95,7 +95,7 @@ $ cargo build -p deltachat_ffi --release
|
||||
|
||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||
|
||||
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
SMTP tracing in addition to info messages.
|
||||
|
||||
### Expensive tests
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# Continuous Integration Scripts for Delta Chat
|
||||
|
||||
Continuous Integration, run through CircleCI and an own build machine.
|
||||
Continuous Integration, run through [GitHub
|
||||
Actions](https://docs.github.com/actions),
|
||||
[CircleCI](https://app.circleci.com/) and an own build machine.
|
||||
|
||||
## Description of scripts
|
||||
|
||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
||||
|
||||
- `../.circleci/config.yml` describing the build jobs that are run
|
||||
by Circle-CI
|
||||
by CircleCI.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
|
||||
@@ -12,8 +12,9 @@ WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
||||
SSHTARGET=ci@b1.delta.chat
|
||||
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||
|
||||
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
|
||||
export BRANCH=${CIRCLE_BRANCH:master}
|
||||
|
||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ if [ -n "$TESTS" ]; then
|
||||
# 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_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
|
||||
@@ -30,12 +30,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
shopt -s huponexit
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
|
||||
docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ ssh $SSHTARGET <<_HERE
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=release
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
set -ex
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen
|
||||
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen
|
||||
|
||||
@@ -39,7 +39,6 @@ mkdir -p $TOXWORKDIR
|
||||
# XXX we may switch on some live-tests on for better ensurances
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
popd
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.51.0-alpha.0"
|
||||
version = "1.52.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -3547,17 +3547,31 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
|
||||
|
||||
/**
|
||||
* Get the name that should be shown over the message (in a group chat) instead of the contact
|
||||
* display name.
|
||||
* display name, or NULL.
|
||||
*
|
||||
* If this returns non-NULL, put a `~` before the override-sender-name and show the
|
||||
* override-sender-name and the sender's avatar even in 1:1 chats.
|
||||
*
|
||||
* In mailing lists, sender display name and sender address do not always belong together.
|
||||
* In this case, this function gives you the name that should actually be shown over the message.
|
||||
*
|
||||
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
|
||||
* Suppose that our user writes an email to support@delta.chat, which forwards to
|
||||
* Bob <bob@delta.chat>, and Bob replies.
|
||||
*
|
||||
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
|
||||
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
|
||||
* as in group messages. If the user then taps on the avatar, they can see that this message
|
||||
* comes from bob@delta.chat.
|
||||
*
|
||||
* You should show a `~` before the override-sender-name in chats, so that the user can
|
||||
* see that this isn't the sender's actual name.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return the name to show over this message or NULL.
|
||||
* If this returns NULL, call `dc_contact_get_display_name()`.
|
||||
* The returned string must be released using dc_str_unref().
|
||||
* Returns an empty string on errors, never returns NULL.
|
||||
*/
|
||||
char* dc_msg_get_override_sender_name(const dc_msg_t* msg);
|
||||
|
||||
|
||||
@@ -156,7 +156,14 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
}
|
||||
let ctx = &*context;
|
||||
match config::Config::from_str(&to_string_lossy(key)) {
|
||||
Ok(key) => block_on(async move { ctx.get_config(key).await.unwrap_or_default().strdup() }),
|
||||
Ok(key) => block_on(async move {
|
||||
ctx.get_config(key)
|
||||
.await
|
||||
.log_err(ctx, "Can't get config")
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}),
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_get_config(): invalid key");
|
||||
"".strdup()
|
||||
@@ -225,8 +232,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let info = ctx.get_info().await;
|
||||
render_info(info).unwrap_or_default().strdup()
|
||||
match ctx.get_info().await {
|
||||
Ok(info) => render_info(info).unwrap_or_default().strdup(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "failed to get info: {}", err);
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,7 +295,12 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move { ctx.is_configured().await as libc::c_int })
|
||||
block_on(async move {
|
||||
ctx.is_configured()
|
||||
.await
|
||||
.log_err(ctx, "failed to get configured state")
|
||||
.unwrap_or_default() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -768,7 +785,12 @@ pub unsafe extern "C" fn dc_set_draft(
|
||||
Some(&mut ffi_msg.message)
|
||||
};
|
||||
|
||||
block_on(ChatId::new(chat_id).set_draft(&ctx, msg))
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.set_draft(&ctx, msg)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to set draft");
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -863,6 +885,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
@@ -876,7 +899,12 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move { ChatId::new(chat_id).get_msg_cnt(&ctx).await as libc::c_int })
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.get_msg_cnt(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get msg count") as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -890,7 +918,12 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await as libc::c_int })
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.get_fresh_msg_cnt(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get fresh msg cnt") as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -988,6 +1021,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_media")
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
@@ -1029,7 +1063,7 @@ pub unsafe extern "C" fn dc_get_next_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
@@ -1122,7 +1156,11 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await);
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
@@ -1148,6 +1186,7 @@ pub unsafe extern "C" fn dc_search_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.search_msgs(chat_id, to_string_lossy(query))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed search_msgs")
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
@@ -1261,7 +1300,8 @@ pub unsafe extern "C" fn dc_set_chat_name(
|
||||
chat_id: u32,
|
||||
name: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || name.is_null() {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null()
|
||||
{
|
||||
eprintln!("ignoring careless call to dc_set_chat_name()");
|
||||
return 0;
|
||||
}
|
||||
@@ -1281,7 +1321,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
|
||||
chat_id: u32,
|
||||
image: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_profile_image()");
|
||||
return 0;
|
||||
}
|
||||
@@ -1406,7 +1446,12 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup()
|
||||
block_on(async move {
|
||||
message::get_msg_info(&ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||
.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1420,7 +1465,9 @@ pub unsafe extern "C" fn dc_get_msg_html(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(MsgId::new(msg_id).get_html(&ctx)).strdup()
|
||||
block_on(MsgId::new(msg_id).get_html(&ctx))
|
||||
.unwrap_or_log_default(ctx, "Failed get_msg_html")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1435,10 +1482,13 @@ pub unsafe extern "C" fn dc_get_mime_headers(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
message::get_mime_headers(&ctx, MsgId::new(msg_id))
|
||||
let mime = message::get_mime_headers(&ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
.unwrap_or_log_default(ctx, "failed to get mime headers");
|
||||
if mime.is_empty() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
mime.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1455,7 +1505,8 @@ pub unsafe extern "C" fn dc_delete_msgs(
|
||||
let ctx = &*context;
|
||||
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]
|
||||
@@ -1468,7 +1519,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
if context.is_null()
|
||||
|| msg_ids.is_null()
|
||||
|| msg_cnt <= 0
|
||||
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32
|
||||
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32()
|
||||
{
|
||||
eprintln!("ignoring careless call to dc_forward_msgs()");
|
||||
return;
|
||||
@@ -1564,14 +1615,12 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(Contact::lookup_id_by_addr(
|
||||
&ctx,
|
||||
to_string_lossy(addr),
|
||||
Origin::IncomingReplyTo,
|
||||
))
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
block_on(async move {
|
||||
Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to lookup id")
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1645,8 +1694,7 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc:
|
||||
block_on(async move {
|
||||
Contact::get_all_blocked(&ctx)
|
||||
.await
|
||||
.log_err(&ctx, "Can't get blocked count")
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_log_default(ctx, "failed to get blocked count")
|
||||
.len() as libc::c_int
|
||||
})
|
||||
}
|
||||
@@ -1931,7 +1979,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
chat_id: u32,
|
||||
seconds: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || seconds < 0 {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 {
|
||||
eprintln!("ignoring careless call to dc_send_locations_to_chat()");
|
||||
return;
|
||||
}
|
||||
@@ -2011,7 +2059,8 @@ pub unsafe extern "C" fn dc_get_locations(
|
||||
timestamp_begin as i64,
|
||||
timestamp_end as i64,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_locations");
|
||||
Box::into_raw(Box::new(dc_array_t::from(res)))
|
||||
})
|
||||
}
|
||||
@@ -2392,8 +2441,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(&ctx).await {
|
||||
Some(p) => p.to_string_lossy().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
Ok(Some(p)) => p.to_string_lossy().strdup(),
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ctx, "failed to get profile image: {:?}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2407,7 +2460,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(ffi_chat.chat.get_color(&ctx))
|
||||
block_on(ffi_chat.chat.get_color(&ctx)).unwrap_or_log_default(ctx, "Failed get_color")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3318,6 +3371,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
|
||||
.contact
|
||||
.get_profile_image(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get profile image")
|
||||
.map(|p| p.to_string_lossy().strdup())
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
})
|
||||
@@ -3549,6 +3603,7 @@ pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(clippy::needless_return)]
|
||||
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_unref()");
|
||||
|
||||
@@ -241,7 +241,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
||||
/// requires that the pointer contains valid UTF-8 on Windows.
|
||||
///
|
||||
/// Because this returns a reference the [Path] silce can not outlive
|
||||
/// Because this returns a reference the [Path] slice can not outlive
|
||||
/// the original pointer.
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[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.13"
|
||||
quote = "1.0.2"
|
||||
@@ -1,43 +0,0 @@
|
||||
#![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)?;
|
||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
@@ -34,7 +34,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM jobs;"))
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
@@ -42,7 +42,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM acpeerstates;"))
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
@@ -50,7 +50,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM keypairs;"))
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
@@ -58,35 +58,34 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM contacts WHERE id>9;"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM chats WHERE id>9;"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM chats_contacts;"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM msgs WHERE id>9;"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
.execute(sqlx::query(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
paramsv![],
|
||||
)
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM leftgrps;"))
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
@@ -120,11 +119,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
real_spec = spec.to_string();
|
||||
context
|
||||
.sql()
|
||||
.set_raw_config(context, "import_spec", Some(&real_spec))
|
||||
.set_raw_config("import_spec", Some(&real_spec))
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config(context, "import_spec").await;
|
||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
return false;
|
||||
@@ -201,7 +200,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == 1 as libc::c_uint {
|
||||
if msg.get_from_id() == 1 {
|
||||
""
|
||||
} else if msg.get_state() == MessageState::InSeen {
|
||||
"[SEEN]"
|
||||
@@ -292,7 +291,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 as libc::c_uint {
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
@@ -543,7 +542,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
chat.get_id().get_fresh_msg_cnt(&context).await,
|
||||
chat.get_id().get_fresh_msg_cnt(&context).await?,
|
||||
if chat.is_muted() { "🔇" } else { "" },
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal => "",
|
||||
@@ -605,7 +604,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "Failed to select chat");
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
@@ -615,7 +614,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
})
|
||||
.collect();
|
||||
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
@@ -638,7 +637,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(&context).await {
|
||||
match sel_chat.get_profile_image(&context).await? {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
@@ -658,14 +657,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!(
|
||||
"{} messages.",
|
||||
sel_chat.get_id().get_msg_cnt(&context).await
|
||||
sel_chat.get_id().get_msg_cnt(&context).await?
|
||||
);
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
}
|
||||
"createchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id: libc::c_int = arg1.parse()?;
|
||||
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
}
|
||||
@@ -716,11 +715,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: libc::c_int = arg1.parse()?;
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
if chat::add_contact_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_0 as u32,
|
||||
contact_id_0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -732,11 +731,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id_1: libc::c_int = arg1.parse()?;
|
||||
let contact_id_1: u32 = arg1.parse()?;
|
||||
chat::remove_contact_from_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
contact_id_1 as u32,
|
||||
contact_id_1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -762,7 +761,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
@@ -787,7 +786,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
let default_marker = "-".to_string();
|
||||
for location in &locations {
|
||||
let marker = location.marker.as_ref().unwrap_or(&default_marker);
|
||||
@@ -899,7 +898,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
None
|
||||
};
|
||||
|
||||
let msglist = context.search_msgs(chat, arg1).await;
|
||||
let msglist = context.search_msgs(chat, arg1).await?;
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
println!("{} messages.", msglist.len());
|
||||
@@ -915,7 +914,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, Some(&mut draft))
|
||||
.await;
|
||||
.await?;
|
||||
println!("Draft saved.");
|
||||
} else {
|
||||
sel_chat
|
||||
@@ -923,7 +922,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(&context, None)
|
||||
.await;
|
||||
.await?;
|
||||
println!("Draft deleted.");
|
||||
}
|
||||
}
|
||||
@@ -946,7 +945,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
println!("{} images or videos: ", images.len());
|
||||
for (i, data) in images.iter().enumerate() {
|
||||
if 0 == i {
|
||||
@@ -1012,7 +1011,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await;
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"html" => {
|
||||
@@ -1021,7 +1020,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let file = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(format!("msg-{}.html", id.to_u32()));
|
||||
let html = id.get_html(&context).await.unwrap_or_default();
|
||||
let html = id.get_html(&context).await?.unwrap_or_default();
|
||||
fs::write(&file, html)?;
|
||||
println!("HTML written to: {:#?}", file);
|
||||
}
|
||||
@@ -1081,14 +1080,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"contactinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
match contact.get_profile_image(&context).await {
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
}
|
||||
@@ -1177,7 +1176,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
// event, event as usize, r as libc::c_int,
|
||||
// event, event as usize, r,
|
||||
// );
|
||||
// }
|
||||
"fileinfo" => {
|
||||
|
||||
@@ -390,7 +390,7 @@ async fn handle_cmd(
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
|
||||
if oauth2_url.is_none() {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
1.51.0
|
||||
------
|
||||
|
||||
- adapt python bindings and APIs to core51 release
|
||||
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
|
||||
for more details on all core changes)
|
||||
|
||||
1.44.0
|
||||
------
|
||||
|
||||
|
||||
@@ -254,17 +254,19 @@ class Chat(object):
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def prepare_message(self, msg):
|
||||
""" create a new prepared message.
|
||||
""" prepare a message for sending.
|
||||
|
||||
:param msg: the message to be prepared.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
:returns: a :class:`deltachat.message.Message` instance.
|
||||
This is the same object that was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be prepared")
|
||||
# invalidate passed in message which is not safe to use anymore
|
||||
msg._dc_msg = msg.id = None
|
||||
return Message.from_db(self.account, msg_id)
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
|
||||
return msg
|
||||
|
||||
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
||||
""" prepare a message for sending and return the resulting Message instance.
|
||||
|
||||
@@ -21,8 +21,8 @@ class Message(object):
|
||||
assert isinstance(dc_msg, ffi.CData)
|
||||
assert dc_msg != ffi.NULL
|
||||
self._dc_msg = dc_msg
|
||||
self.id = lib.dc_msg_get_id(dc_msg)
|
||||
assert self.id is not None and self.id >= 0, repr(self.id)
|
||||
msg_id = self.id
|
||||
assert msg_id is not None and msg_id >= 0, repr(msg_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.account == other.account and self.id == other.id
|
||||
@@ -46,9 +46,13 @@ class Message(object):
|
||||
def new_empty(cls, account, view_type):
|
||||
""" create a non-persistent message.
|
||||
|
||||
:param: view_type is "text", "audio", "video", "file"
|
||||
:param: view_type is the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker"
|
||||
"""
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
else:
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
return Message(account, ffi.gc(
|
||||
lib.dc_msg_new(account._dc_context, view_type_code),
|
||||
lib.dc_msg_unref
|
||||
@@ -68,6 +72,11 @@ class Message(object):
|
||||
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def id(self):
|
||||
"""id of this message. """
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
@@ -339,6 +348,10 @@ class Message(object):
|
||||
""" return True if it's a gif message. """
|
||||
return self._view_type == const.DC_MSG_GIF
|
||||
|
||||
def is_sticker(self):
|
||||
""" return True if it's a sticker message. """
|
||||
return self._view_type == const.DC_MSG_STICKER
|
||||
|
||||
def is_audio(self):
|
||||
""" return True if it's an audio message. """
|
||||
return self._view_type == const.DC_MSG_AUDIO
|
||||
@@ -359,21 +372,22 @@ class Message(object):
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
_view_type_mapping = {
|
||||
const.DC_MSG_TEXT: 'text',
|
||||
const.DC_MSG_IMAGE: 'image',
|
||||
const.DC_MSG_GIF: 'gif',
|
||||
const.DC_MSG_AUDIO: 'audio',
|
||||
const.DC_MSG_VIDEO: 'video',
|
||||
const.DC_MSG_FILE: 'file'
|
||||
'text': const.DC_MSG_TEXT,
|
||||
'image': const.DC_MSG_IMAGE,
|
||||
'gif': const.DC_MSG_GIF,
|
||||
'audio': const.DC_MSG_AUDIO,
|
||||
'video': const.DC_MSG_VIDEO,
|
||||
'file': const.DC_MSG_FILE,
|
||||
'sticker': const.DC_MSG_STICKER,
|
||||
}
|
||||
|
||||
|
||||
def get_viewtype_code_from_name(view_type_name):
|
||||
for code, value in _view_type_mapping.items():
|
||||
if value == view_type_name:
|
||||
return code
|
||||
code = _view_type_mapping.get(view_type_name)
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError("message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -2187,9 +2187,25 @@ class TestOnlineAccount:
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1.set_config("selfstatus", "New status")
|
||||
chat12.send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.get_sender_contact().status == "New status"
|
||||
msg_received = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_received.text == "hi"
|
||||
assert msg_received.get_sender_contact().status == "New status"
|
||||
|
||||
# Send a reply from ac2 to ac1 so ac1 can send a read receipt.
|
||||
reply_msg = msg_received.chat.send_text("reply")
|
||||
reply_msg_received = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply_msg_received.text == "reply"
|
||||
|
||||
# Send read receipt from ac1 to ac2.
|
||||
# It does not contain the signature.
|
||||
ac1.mark_seen_messages([reply_msg_received])
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 == reply_msg.chat.id
|
||||
assert ev.data2 == reply_msg.id
|
||||
assert reply_msg.is_out_mdn_received()
|
||||
|
||||
# Test that the status is not cleared as a result of receiving a read receipt.
|
||||
assert msg_received.get_sender_contact().status == "New status"
|
||||
|
||||
ac1.set_config("selfstatus", "")
|
||||
chat12.send_text("hello")
|
||||
|
||||
@@ -13,7 +13,6 @@ passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
|
||||
@@ -23,9 +23,6 @@ if [ $? != 0 ]; then
|
||||
fi
|
||||
|
||||
pushd python
|
||||
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
|
||||
export DCC_PY_LIVECONFIG=liveconfig
|
||||
fi
|
||||
tox "$@"
|
||||
ret=$?
|
||||
popd
|
||||
|
||||
@@ -135,15 +135,25 @@ impl Accounts {
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
// create new account
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
let account_config = self
|
||||
.config
|
||||
.new_account(&self.dir)
|
||||
.await
|
||||
.context("failed to create new account")?;
|
||||
|
||||
let new_dbfile = account_config.dbfile().into();
|
||||
let new_blobdir = Context::derive_blobdir(&new_dbfile);
|
||||
|
||||
let res = {
|
||||
fs::create_dir_all(&account_config.dir).await?;
|
||||
fs::rename(&dbfile, &new_dbfile).await?;
|
||||
fs::rename(&blobdir, &new_blobdir).await?;
|
||||
fs::create_dir_all(&account_config.dir)
|
||||
.await
|
||||
.context("failed to create dir")?;
|
||||
fs::rename(&dbfile, &new_dbfile)
|
||||
.await
|
||||
.context("failed to rename dbfile")?;
|
||||
fs::rename(&blobdir, &new_blobdir)
|
||||
.await
|
||||
.context("failed to rename blobdir")?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
@@ -502,7 +512,10 @@ mod tests {
|
||||
let ctx = accounts.get_selected_account().await;
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr).await.unwrap()
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ impl<'a> BlobObject<'a> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
|
||||
let img_wh =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
|
||||
@@ -403,7 +403,7 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
let img_wh =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
|
||||
@@ -514,6 +514,10 @@ pub enum BlobError {
|
||||
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||
#[error("Blob has a badname {}", .blobname.display())]
|
||||
WrongName { blobname: PathBuf },
|
||||
#[error("Sql: {0}")]
|
||||
Sql(#[from] crate::sql::Error),
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
1412
src/chat.rs
1412
src/chat.rs
File diff suppressed because it is too large
Load Diff
277
src/chatlist.rs
277
src/chatlist.rs
@@ -1,6 +1,8 @@
|
||||
//! # Chat list module
|
||||
|
||||
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};
|
||||
@@ -110,17 +112,6 @@ impl Chatlist {
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
@@ -130,6 +121,13 @@ impl Chatlist {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
let process_row = |row: sqlx::Result<sqlx::sqlite::SqliteRow>| {
|
||||
let row = row?;
|
||||
let chat_id: ChatId = row.try_get(0)?;
|
||||
let msg_id: MsgId = row.try_get(1).unwrap_or_default();
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
@@ -145,10 +143,10 @@ impl Chatlist {
|
||||
// tg do the same) for the deaddrop, however, they should
|
||||
// really be hidden, however, _currently_ the deaddrop is not
|
||||
// shown at all permanent in the chatlist.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
context.sql.fetch(
|
||||
sqlx::query("SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
@@ -162,11 +160,9 @@ impl Chatlist {
|
||||
AND c.blocked=0
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
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)
|
||||
).await?.map(process_row).collect::<sqlx::Result<_>>().await?
|
||||
} else if flag_archived_only {
|
||||
// show archived chats
|
||||
// (this includes the archived device-chat; we could skip it,
|
||||
@@ -174,8 +170,9 @@ impl Chatlist {
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
@@ -190,11 +187,13 @@ impl Chatlist {
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.bind(MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
.map(process_row)
|
||||
.collect::<sqlx::Result<_>>()
|
||||
.await?
|
||||
} else if let Some(query) = query {
|
||||
let query = query.trim().to_string();
|
||||
ensure!(!query.is_empty(), "missing query");
|
||||
@@ -208,8 +207,9 @@ impl Chatlist {
|
||||
let str_like_cmd = format!("%{}%", query);
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
@@ -224,11 +224,15 @@ impl Chatlist {
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.bind(MessageState::OutDraft)
|
||||
.bind(skip_id)
|
||||
.bind(str_like_cmd),
|
||||
)
|
||||
.await?
|
||||
.map(process_row)
|
||||
.collect::<sqlx::Result<_>>()
|
||||
.await?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if flag_for_forwarding {
|
||||
@@ -239,7 +243,8 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
|
||||
let mut ids: Vec<_> = context.sql.fetch(sqlx::query(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -254,19 +259,21 @@ impl Chatlist {
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?3
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
|
||||
)
|
||||
.bind(MessageState::OutDraft)
|
||||
.bind(skip_id)
|
||||
.bind(ChatVisibility::Archived)
|
||||
.bind(sort_id_up)
|
||||
.bind(ChatVisibility::Pinned)
|
||||
).await?.map(process_row).collect::<sqlx::Result<_>>().await?;
|
||||
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||
if let Some(last_deaddrop_fresh_msg_id) =
|
||||
get_last_deaddrop_fresh_msg(context).await?
|
||||
{
|
||||
if !flag_for_forwarding {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
|
||||
}
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
@@ -274,11 +281,11 @@ impl Chatlist {
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
|
||||
}
|
||||
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
|
||||
}
|
||||
|
||||
Ok(Chatlist { ids })
|
||||
@@ -400,46 +407,44 @@ impl Chatlist {
|
||||
}
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
context
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.count(sqlx::query(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
))
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
|
||||
// We have an index over the state-column, this should be
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
context
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.query_get_value(sqlx::query(concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
)))
|
||||
.await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message;
|
||||
use crate::message::ContactRequestDecision;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
@@ -466,7 +471,7 @@ mod tests {
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await;
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
@@ -545,6 +550,136 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_single_chat() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message, accept contact request
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
|
||||
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Authname");
|
||||
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Nickname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// revert contact to authname, this again changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Authname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// receive a one-to-one-message without authname set, accept contact request
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg5678@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat_id =
|
||||
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
|
||||
.await
|
||||
.unwrap();
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = *contacts.first().unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "bob@example.org");
|
||||
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Bob Nickname");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
|
||||
// revert name change, this again changes the name of the one-to-one-chat to the email-address
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
assert_eq!(contact_id, test_id);
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "bob@example.org");
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// finally, also check that a simple substring-search is working with email-addresses
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -554,7 +689,7 @@ mod tests {
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await;
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t, 0, None).await;
|
||||
|
||||
115
src/config.rs
115
src/config.rs
@@ -1,5 +1,6 @@
|
||||
//! # Key-value configuration management
|
||||
|
||||
use anyhow::Result;
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
@@ -150,69 +151,66 @@ pub enum Config {
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> bool {
|
||||
self.sql.get_raw_config(self, key).await.is_some()
|
||||
pub async fn config_exists(&self, key: Config) -> Result<bool> {
|
||||
Ok(self.sql.get_raw_config(key).await?.is_some())
|
||||
}
|
||||
|
||||
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||
pub async fn get_config(&self, key: Config) -> Option<String> {
|
||||
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(self, key).await;
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(self, key).await,
|
||||
_ => self.sql.get_raw_config(key).await?,
|
||||
};
|
||||
|
||||
if value.is_some() {
|
||||
return value;
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
// Default values
|
||||
match key {
|
||||
Config::Selfstatus => Some(stock_str::status_line(self).await),
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
|
||||
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
|
||||
_ => Ok(key.get_str("default").map(|s| s.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_config_int(&self, key: Config) -> i32 {
|
||||
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default()
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_i64(&self, key: Config) -> i64 {
|
||||
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default()
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_u64(&self, key: Config) -> u64 {
|
||||
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
|
||||
self.get_config(key)
|
||||
.await
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default()
|
||||
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub async fn get_config_bool(&self, key: Config) -> bool {
|
||||
self.get_config_int(key).await != 0
|
||||
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
|
||||
Ok(self.get_config_int(key).await? != 0)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await {
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x as i64),
|
||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await? {
|
||||
0 => Ok(None),
|
||||
1 => Ok(Some(0)),
|
||||
x => Ok(Some(x as i64)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,41 +218,46 @@ impl Context {
|
||||
///
|
||||
/// The provider is determined by `get_provider_info()` during configuration and then saved
|
||||
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
|
||||
pub async fn get_configured_provider(&self) -> Option<&'static Provider> {
|
||||
get_provider_by_id(&self.get_config(Config::ConfiguredProvider).await?)
|
||||
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
|
||||
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
|
||||
return Ok(get_provider_by_id(&cfg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
/// after `x` seconds.
|
||||
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await {
|
||||
0 => None,
|
||||
x => Some(x as i64),
|
||||
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await? {
|
||||
0 => Ok(None),
|
||||
x => Ok(Some(x as i64)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.execute(sqlx::query("UPDATE contacts SET selfavatar_sent=0;"))
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||
.set_raw_config_bool("attach_selfavatar", true)
|
||||
.await?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let blob = BlobObject::new_from_path(self, value).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql
|
||||
.set_raw_config(self, key, Some(blob.as_name()))
|
||||
.await
|
||||
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
self.sql.set_raw_config(key, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => self.sql.set_raw_config(self, key, None).await,
|
||||
}
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
@@ -265,10 +268,15 @@ impl Context {
|
||||
value
|
||||
};
|
||||
|
||||
self.sql.set_raw_config(self, key, val).await
|
||||
self.sql.set_raw_config(key, val).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
@@ -278,20 +286,29 @@ impl Context {
|
||||
}
|
||||
Config::Displayname => {
|
||||
let value = value.map(improve_single_line_input);
|
||||
self.sql.set_raw_config(self, key, value.as_deref()).await
|
||||
self.sql.set_raw_config(key, value.as_deref()).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteServerAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
job::schedule_resync(self).await;
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value).await,
|
||||
_ => {
|
||||
self.sql.set_raw_config(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
|
||||
self.set_config(key, if value { Some("1") } else { None })
|
||||
.await
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +366,7 @@ mod tests {
|
||||
.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;
|
||||
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();
|
||||
@@ -378,7 +395,7 @@ mod tests {
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await;
|
||||
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();
|
||||
@@ -405,21 +422,21 @@ mod tests {
|
||||
std::fs::metadata(&avatar_blob).unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await;
|
||||
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 fn test_media_quality_config_option() {
|
||||
let t = TestContext::new().await;
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await;
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
|
||||
assert_eq!(media_quality, 0);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
assert_eq!(media_quality, constants::MediaQuality::Balanced);
|
||||
|
||||
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
|
||||
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await;
|
||||
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
|
||||
assert_eq!(media_quality, 1);
|
||||
assert_eq!(constants::MediaQuality::Worse as i32, 1);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
|
||||
@@ -115,8 +115,8 @@ fn parse_server<B: BufRead>(
|
||||
MozConfigTag::Username => username = Some(val),
|
||||
MozConfigTag::Sockettype => {
|
||||
sockettype = match val.to_lowercase().as_ref() {
|
||||
"ssl" => Socket::SSL,
|
||||
"starttls" => Socket::STARTTLS,
|
||||
"ssl" => Socket::Ssl,
|
||||
"starttls" => Socket::Starttls,
|
||||
"plain" => Socket::Plain,
|
||||
_ => Socket::Automatic,
|
||||
}
|
||||
@@ -233,8 +233,8 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
|
||||
.chain(moz_ac.outgoing_servers.into_iter())
|
||||
.filter_map(|server| {
|
||||
let protocol = match server.typ.as_ref() {
|
||||
"imap" => Some(Protocol::IMAP),
|
||||
"smtp" => Some(Protocol::SMTP),
|
||||
"imap" => Some(Protocol::Imap),
|
||||
"smtp" => Some(Protocol::Smtp),
|
||||
_ => None,
|
||||
};
|
||||
Some(ServerParams {
|
||||
@@ -276,10 +276,10 @@ mod tests {
|
||||
fn test_parse_outlook_autoconfig() {
|
||||
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
|
||||
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
|
||||
assert_eq!(res[0].protocol, Protocol::IMAP);
|
||||
assert_eq!(res[0].protocol, Protocol::Imap);
|
||||
assert_eq!(res[0].hostname, "outlook.office365.com");
|
||||
assert_eq!(res[0].port, 993);
|
||||
assert_eq!(res[1].protocol, Protocol::SMTP);
|
||||
assert_eq!(res[1].protocol, Protocol::Smtp);
|
||||
assert_eq!(res[1].hostname, "smtp.office365.com");
|
||||
assert_eq!(res[1].port, 587);
|
||||
}
|
||||
@@ -295,25 +295,25 @@ mod tests {
|
||||
assert_eq!(res.incoming_servers[0].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[0].port, 993);
|
||||
assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL);
|
||||
assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
|
||||
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[1].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[1].port, 143);
|
||||
assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[2].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[2].port, 995);
|
||||
assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL);
|
||||
assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
|
||||
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[3].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[3].port, 110);
|
||||
assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.outgoing_servers.len(), 1);
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
assert_eq!(res.outgoing_servers[0].typ, "smtp");
|
||||
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.outgoing_servers[0].port, 587);
|
||||
assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
|
||||
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +169,8 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
|
||||
.filter_map(|protocol| {
|
||||
Some(ServerParams {
|
||||
protocol: match protocol.typ.to_lowercase().as_ref() {
|
||||
"imap" => Some(Protocol::IMAP),
|
||||
"smtp" => Some(Protocol::SMTP),
|
||||
"imap" => Some(Protocol::Imap),
|
||||
"smtp" => Some(Protocol::Smtp),
|
||||
_ => None,
|
||||
}?,
|
||||
socket: match protocol.ssl {
|
||||
|
||||
@@ -50,8 +50,11 @@ macro_rules! progress {
|
||||
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> bool {
|
||||
self.sql.get_raw_config_bool(self, "configured").await
|
||||
pub async fn is_configured(&self) -> Result<bool> {
|
||||
self.sql
|
||||
.get_raw_config_bool("configured")
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
@@ -84,14 +87,14 @@ impl Context {
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::from_database(self, "").await;
|
||||
let mut param = LoginParam::from_database(self, "").await?;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await {
|
||||
if !self.config_exists(def.key).await? {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
@@ -177,13 +180,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await
|
||||
.await?
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
info!(ctx, "Authorized address is {}", oauth2_addr);
|
||||
param.addr = oauth2_addr;
|
||||
ctx.sql
|
||||
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
|
||||
.set_raw_config("addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
}
|
||||
progress!(ctx, 20);
|
||||
@@ -217,7 +220,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
if let Some(provider) = provider::get_provider_info(¶m_domain).await {
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::OK | provider::Status::PREPARATION => {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "offline autoconfig found, but no servers defined");
|
||||
param_autoconfig = None;
|
||||
@@ -232,8 +235,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::EMAIL => param.addr.to_string(),
|
||||
UsernamePattern::EMAILLOCALPART => {
|
||||
UsernamePattern::Email => param.addr.to_string(),
|
||||
UsernamePattern::Emaillocalpart => {
|
||||
if let Some(at) = param.addr.find('@') {
|
||||
param.addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
@@ -247,7 +250,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
param_autoconfig = Some(servers)
|
||||
}
|
||||
}
|
||||
provider::Status::BROKEN => {
|
||||
provider::Status::Broken => {
|
||||
info!(ctx, "offline autoconfig found, provider is broken");
|
||||
param_autoconfig = None;
|
||||
}
|
||||
@@ -266,10 +269,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
if !servers
|
||||
.iter()
|
||||
.any(|server| server.protocol == Protocol::IMAP)
|
||||
.any(|server| server.protocol == Protocol::Imap)
|
||||
{
|
||||
servers.push(ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
protocol: Protocol::Imap,
|
||||
hostname: param.imap.server.clone(),
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
@@ -278,10 +281,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
if !servers
|
||||
.iter()
|
||||
.any(|server| server.protocol == Protocol::SMTP)
|
||||
.any(|server| server.protocol == Protocol::Smtp)
|
||||
{
|
||||
servers.push(ServerParams {
|
||||
protocol: Protocol::SMTP,
|
||||
protocol: Protocol::Smtp,
|
||||
hostname: param.smtp.server.clone(),
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
@@ -300,7 +303,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let smtp_addr = param.addr.clone();
|
||||
let smtp_servers: Vec<ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::SMTP)
|
||||
.filter(|params| params.protocol == Protocol::Smtp)
|
||||
.cloned()
|
||||
.collect();
|
||||
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
|
||||
@@ -348,7 +351,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut imap_configured = false;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::IMAP)
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
.collect();
|
||||
let imap_servers_count = imap_servers.len();
|
||||
let mut errors = Vec::new();
|
||||
@@ -397,8 +400,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await;
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
@@ -413,7 +416,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
// "configured_" prefix; also write the "configured"-flag */
|
||||
// the trailing underscore is correct
|
||||
param.save_to_database(ctx, "configured_").await?;
|
||||
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ impl ServerParams {
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = match self.protocol {
|
||||
Protocol::IMAP => "imap.".to_string() + param_domain,
|
||||
Protocol::SMTP => "smtp.".to_string() + param_domain,
|
||||
Protocol::Imap => "imap.".to_string() + param_domain,
|
||||
Protocol::Smtp => "smtp.".to_string() + param_domain,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
@@ -66,13 +66,13 @@ impl ServerParams {
|
||||
// Try to infer port from socket security.
|
||||
if self.port == 0 {
|
||||
self.port = match self.socket {
|
||||
Socket::SSL => match self.protocol {
|
||||
Protocol::IMAP => 993,
|
||||
Protocol::SMTP => 465,
|
||||
Socket::Ssl => match self.protocol {
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
},
|
||||
Socket::STARTTLS | Socket::Plain => match self.protocol {
|
||||
Protocol::IMAP => 143,
|
||||
Protocol::SMTP => 587,
|
||||
Socket::Starttls | Socket::Plain => match self.protocol {
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
},
|
||||
Socket::Automatic => 0,
|
||||
}
|
||||
@@ -85,27 +85,27 @@ impl ServerParams {
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try STARTTLS
|
||||
self.socket = Socket::STARTTLS;
|
||||
self.socket = Socket::Starttls;
|
||||
self.port = match self.protocol {
|
||||
Protocol::IMAP => 143,
|
||||
Protocol::SMTP => 587,
|
||||
Protocol::Imap => 143,
|
||||
Protocol::Smtp => 587,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
// Try TLS
|
||||
self.socket = Socket::SSL;
|
||||
self.socket = Socket::Ssl;
|
||||
self.port = match self.protocol {
|
||||
Protocol::IMAP => 993,
|
||||
Protocol::SMTP => 465,
|
||||
Protocol::Imap => 993,
|
||||
Protocol::Smtp => 465,
|
||||
};
|
||||
res.push(self);
|
||||
} else if self.socket == Socket::Automatic {
|
||||
// Try TLS over user-provided port.
|
||||
self.socket = Socket::SSL;
|
||||
self.socket = Socket::Ssl;
|
||||
res.push(self.clone());
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
self.socket = Socket::STARTTLS;
|
||||
self.socket = Socket::Starttls;
|
||||
res.push(self);
|
||||
} else {
|
||||
res.push(self);
|
||||
@@ -140,10 +140,10 @@ mod tests {
|
||||
fn test_expand_param_vector() {
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
protocol: Protocol::Imap,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 0,
|
||||
socket: Socket::SSL,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
@@ -153,10 +153,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
protocol: Protocol::Imap,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 993,
|
||||
socket: Socket::SSL,
|
||||
socket: Socket::Ssl,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! # Constants
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
#[derive(
|
||||
@@ -14,12 +15,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
#[repr(i8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
@@ -32,9 +32,7 @@ impl Default for Blocked {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
@@ -48,9 +46,7 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
@@ -63,9 +59,7 @@ impl Default for MediaQuality {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
Default = 0,
|
||||
@@ -79,9 +73,7 @@ impl Default for KeyGenType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
Unknown = 0,
|
||||
@@ -122,15 +114,15 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
|
||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
|
||||
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: u32 = 3;
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
/// only an indicator in a chatlist
|
||||
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
|
||||
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
|
||||
/// only an indicator in a chatlist
|
||||
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
|
||||
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
|
||||
/// larger chat IDs are "real" chats, their messages are "real" messages.
|
||||
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -141,11 +133,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
IntoStaticStr,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
@@ -256,12 +247,11 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum Viewtype {
|
||||
Unknown = 0,
|
||||
|
||||
|
||||
479
src/contact.rs
479
src/contact.rs
@@ -1,11 +1,13 @@
|
||||
//! Contacts module
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use async_std::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -77,9 +79,9 @@ pub struct Contact {
|
||||
|
||||
/// Possible origins of a contact.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum Origin {
|
||||
Unknown = 0,
|
||||
|
||||
@@ -174,43 +176,45 @@ pub enum VerifiedStatus {
|
||||
|
||||
impl Contact {
|
||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
||||
let mut res = context
|
||||
let row = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
.fetch_one(
|
||||
sqlx::query(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
|row| {
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name: row.get::<_, String>(0)?,
|
||||
authname: row.get::<_, String>(4)?,
|
||||
addr: row.get::<_, String>(1)?,
|
||||
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
|
||||
origin: row.get(2)?,
|
||||
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
|
||||
status: row.get(6).unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
},
|
||||
)
|
||||
.bind(contact_id),
|
||||
)
|
||||
.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 {
|
||||
res.name = stock_str::self_msg(context).await;
|
||||
res.addr = context
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res.status = context
|
||||
contact.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
} else if contact_id == DC_CONTACT_ID_DEVICE {
|
||||
res.name = stock_str::device_messages(context).await;
|
||||
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
|
||||
contact.name = stock_str::device_messages(context).await;
|
||||
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
|
||||
}
|
||||
Ok(res)
|
||||
|
||||
Ok(contact)
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact is blocked.
|
||||
@@ -281,13 +285,15 @@ impl Contact {
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;")
|
||||
.bind(MessageState::InNoticed)
|
||||
.bind(id as i32)
|
||||
.bind(MessageState::InFresh),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
|
||||
context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,21 +314,27 @@ impl Contact {
|
||||
|
||||
let addr_normalized = addr_normalize(addr.as_ref());
|
||||
|
||||
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await {
|
||||
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
return Ok(Some(DC_CONTACT_ID_SELF));
|
||||
}
|
||||
}
|
||||
context.sql.query_get_value_result(
|
||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![
|
||||
addr_normalized,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
min_origin as u32,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.context("lookup_id_by_addr: SQL query failed")
|
||||
let id = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
sqlx::query(
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
)
|
||||
.bind(addr_normalized)
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
||||
.bind(min_origin),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Lookup a contact and create it if it does not exist yet.
|
||||
@@ -367,7 +379,7 @@ impl Contact {
|
||||
let addr = addr_normalize(addr.as_ref()).to_string();
|
||||
let addr_self = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(&addr, addr_self) {
|
||||
@@ -419,25 +431,33 @@ impl Contact {
|
||||
let mut update_addr = false;
|
||||
let mut row_id = 0;
|
||||
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
|
||||
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
paramsv![addr.to_string()],
|
||||
|row| {
|
||||
let row_id = 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)?;
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||
.sql
|
||||
.fetch_one(
|
||||
sqlx::query(
|
||||
"SELECT id, name, addr, origin, authname \
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
)
|
||||
.bind(addr.to_string()),
|
||||
)
|
||||
.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))
|
||||
},
|
||||
)
|
||||
.await {
|
||||
})
|
||||
{
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname =
|
||||
!manual && name != row_authname && !name.is_empty() &&
|
||||
(origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty());
|
||||
|
||||
let update_authname = !manual
|
||||
&& name != row_authname
|
||||
&& !name.is_empty()
|
||||
&& (origin >= row_origin
|
||||
|| origin == Origin::IncomingUnknownFrom
|
||||
|| row_authname.is_empty());
|
||||
row_id = id;
|
||||
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
||||
update_addr = true;
|
||||
@@ -449,43 +469,57 @@ impl Contact {
|
||||
row_name
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
paramsv![
|
||||
new_name,
|
||||
if update_addr { addr.to_string() } else { row_addr },
|
||||
if origin > row_origin {
|
||||
origin
|
||||
} else {
|
||||
row_origin
|
||||
},
|
||||
if update_authname {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_authname
|
||||
},
|
||||
row_id
|
||||
],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
let query = sqlx::query(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
)
|
||||
.bind(&new_name)
|
||||
.bind(if update_addr {
|
||||
addr.to_string()
|
||||
} else {
|
||||
row_addr
|
||||
})
|
||||
.bind(if origin > row_origin {
|
||||
origin
|
||||
} else {
|
||||
row_origin
|
||||
})
|
||||
.bind(if update_authname {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_authname
|
||||
})
|
||||
.bind(row_id);
|
||||
|
||||
context.sql.execute(query).await.ok();
|
||||
|
||||
if update_name {
|
||||
// Update the contact name also if it is used as a group name.
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id = context.sql.query_get_value::<i32>(
|
||||
context,
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
paramsv![Chattype::Single, row_id]
|
||||
).await;
|
||||
let chat_id = context.sql.query_get_value::<_, u32>(
|
||||
sqlx::query(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)"
|
||||
).bind(Chattype::Single).bind(row_id)
|
||||
).await?;
|
||||
if let Some(chat_id) = chat_id {
|
||||
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
|
||||
let contact = Contact::get_by_id(context, row_id as u32).await?;
|
||||
let chat_name = contact.get_display_name();
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3")
|
||||
.bind(&chat_name)
|
||||
.bind(chat_id)
|
||||
.bind(&chat_name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => warn!(context, "Can't update chat name: {}", err),
|
||||
Ok(count) => if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context
|
||||
.emit_event(EventType::ChatModified(ChatId::new(chat_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,21 +533,26 @@ impl Contact {
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
||||
paramsv![
|
||||
if update_name { name.to_string() } else { "".to_string() },
|
||||
addr,
|
||||
origin,
|
||||
if update_authname { name.to_string() } else { "".to_string() }
|
||||
],
|
||||
sqlx::query(
|
||||
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
||||
)
|
||||
.bind(if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
})
|
||||
.bind(&addr)
|
||||
.bind(origin)
|
||||
.bind(if update_authname {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
row_id = context
|
||||
.sql
|
||||
.get_rowid(context, "contacts", "addr", &addr)
|
||||
.await?;
|
||||
row_id = context.sql.get_rowid("contacts", "addr", &addr).await?;
|
||||
sth_modified = Modifier::Created;
|
||||
info!(context, "added contact id={} addr={}", row_id, &addr);
|
||||
} else {
|
||||
@@ -521,7 +560,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((row_id, sth_modified))
|
||||
Ok((u32::try_from(row_id)?, sth_modified))
|
||||
}
|
||||
|
||||
/// Add a number of contacts.
|
||||
@@ -584,7 +623,7 @@ impl Contact {
|
||||
) -> Result<Vec<u32>> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut add_self = false;
|
||||
@@ -600,10 +639,12 @@ impl Contact {
|
||||
.map(|s| s.as_ref().to_string())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
context
|
||||
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id FROM contacts c \
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr!=?1 \
|
||||
AND c.id>?2 \
|
||||
@@ -612,27 +653,23 @@ impl Contact {
|
||||
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) \
|
||||
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||
paramsv![
|
||||
self_addr,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.bind(&self_addr)
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
||||
.bind(Origin::IncomingReplyTo)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(if flag_verified_only { 0i32 } else { 1i32 }),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.map(|row| row?.try_get(0));
|
||||
while let Some(id) = rows.next().await {
|
||||
ret.push(id?);
|
||||
}
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let self_name2 = stock_str::self_msg(context);
|
||||
|
||||
@@ -649,25 +686,27 @@ impl Contact {
|
||||
} else {
|
||||
add_self = true;
|
||||
|
||||
context
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr!=?1
|
||||
AND id>?2
|
||||
AND origin>=?3
|
||||
AND blocked=0
|
||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.bind(self_addr)
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL)
|
||||
.bind(Origin::IncomingReplyTo),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.map(|row| row?.try_get(0));
|
||||
|
||||
while let Some(id) = rows.next().await {
|
||||
ret.push(id?);
|
||||
}
|
||||
}
|
||||
|
||||
if flag_add_self && add_self {
|
||||
@@ -683,41 +722,55 @@ impl Contact {
|
||||
// from the users perspective,
|
||||
// there is not much difference in an email- and a mailinglist-address)
|
||||
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
|
||||
let blocked_mailinglists = context
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
|
||||
paramsv![Chattype::Mailinglist, Blocked::Manually],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
.fetch(
|
||||
sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;")
|
||||
.bind(Chattype::Mailinglist)
|
||||
.bind(Blocked::Manually),
|
||||
)
|
||||
.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
|
||||
.sql
|
||||
.exists("SELECT id FROM contacts WHERE addr=?;", paramsv![grpid])
|
||||
.exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid))
|
||||
.await?
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.execute("INSERT INTO contacts (addr) VALUES (?);", paramsv![grpid])
|
||||
.execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid))
|
||||
.await?;
|
||||
}
|
||||
// always do an update in case the blocking is reset or name is changed
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;",
|
||||
paramsv![name, Origin::MailinglistAddress, grpid],
|
||||
sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;")
|
||||
.bind(name)
|
||||
.bind(Origin::MailinglistAddress)
|
||||
.bind(&grpid),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0")
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL),
|
||||
)
|
||||
.await?;
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
|
||||
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
|
||||
@@ -727,19 +780,19 @@ impl Contact {
|
||||
);
|
||||
}
|
||||
|
||||
let ret = context
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
|row| row.get::<_, u32>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
).bind(DC_CONTACT_ID_LAST_SPECIAL)
|
||||
)
|
||||
.await?
|
||||
.map(|row| row?.try_get::<u32, _>(0))
|
||||
.collect::<sqlx::Result<Vec<_>>>()
|
||||
.await?;
|
||||
Ok(ret)
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a textual summary of the encryption state for the contact.
|
||||
@@ -755,7 +808,7 @@ impl Contact {
|
||||
|
||||
let mut ret = String::new();
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
if let Some(peerstate) = peerstate.filter(|peerstate| {
|
||||
@@ -822,26 +875,23 @@ impl Contact {
|
||||
"Can not delete special contact"
|
||||
);
|
||||
|
||||
let count_contacts: i32 = context
|
||||
let count_contacts = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
.count(
|
||||
sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;")
|
||||
.bind(contact_id),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
.await?;
|
||||
|
||||
let count_msgs: i32 = if count_contacts > 0 {
|
||||
let count_msgs = if count_contacts > 0 {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||
paramsv![contact_id as i32, contact_id as i32],
|
||||
.count(
|
||||
sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;")
|
||||
.bind(contact_id)
|
||||
.bind(contact_id),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -849,10 +899,7 @@ impl Contact {
|
||||
if count_msgs == 0 {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
@@ -889,8 +936,9 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id as i32],
|
||||
sqlx::query("UPDATE contacts SET param=? WHERE id=?")
|
||||
.bind(self.param.to_string())
|
||||
.bind(self.id as i32),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -901,8 +949,9 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET status=? WHERE id=?",
|
||||
paramsv![self.status, self.id as i32],
|
||||
sqlx::query("UPDATE contacts SET status=? WHERE id=?")
|
||||
.bind(&self.status)
|
||||
.bind(self.id as i32),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -967,17 +1016,17 @@ impl Contact {
|
||||
/// Get the contact's profile image.
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using dc_set_config(context, "selfavatar", image).
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if self.id == DC_CONTACT_ID_SELF {
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await {
|
||||
return Some(PathBuf::from(p));
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await? {
|
||||
return Ok(Some(PathBuf::from(p)));
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Some(dc_get_abs_path(context, image_rel));
|
||||
return Ok(Some(dc_get_abs_path(context, image_rel)));
|
||||
}
|
||||
}
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get a color for the contact.
|
||||
@@ -1065,20 +1114,19 @@ impl Contact {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn get_real_cnt(context: &Context) -> usize {
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
return 0;
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
context
|
||||
let count = context
|
||||
.sql
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
.count(
|
||||
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;")
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default() as usize
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
||||
@@ -1088,10 +1136,7 @@ impl Contact {
|
||||
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1100,8 +1145,10 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
sqlx::query("UPDATE contacts SET origin=? WHERE id=? AND origin<?;")
|
||||
.bind(origin)
|
||||
.bind(contact_id)
|
||||
.bind(origin),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1155,8 +1202,9 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
&& context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
sqlx::query("UPDATE contacts SET blocked=? WHERE id=?;")
|
||||
.bind(new_blocking as i32)
|
||||
.bind(contact_id),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1166,9 +1214,24 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context.sql.execute(
|
||||
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
paramsv![new_blocking, 100, contact_id as i32]).await.is_ok()
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET blocked=?
|
||||
WHERE type=? AND id IN (
|
||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.bind(new_blocking)
|
||||
.bind(Chattype::Single)
|
||||
.bind(contact_id),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
@@ -1299,7 +1362,7 @@ impl Context {
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
let self_addr = self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
|
||||
270
src/context.rs
270
src/context.rs
@@ -6,12 +6,14 @@ use std::ops::Deref;
|
||||
use std::time::{Instant, SystemTime};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
task,
|
||||
};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::chat::{get_chat_cnt, ChatId};
|
||||
use crate::config::Config;
|
||||
@@ -89,8 +91,9 @@ pub struct RunningState {
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("sqlite_version", crate::sql::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
res.insert("num_cpus", num_cpus::get().to_string());
|
||||
res.insert("level", "awesome".into());
|
||||
res
|
||||
}
|
||||
@@ -270,68 +273,68 @@ impl Context {
|
||||
* UI chat/message related API
|
||||
******************************************************************************/
|
||||
|
||||
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let unset = "0";
|
||||
let l = LoginParam::from_database(self, "").await;
|
||||
let l2 = LoginParam::from_database(self, "configured_").await;
|
||||
let displayname = self.get_config(Config::Displayname).await;
|
||||
let chats = get_chat_cnt(self).await as usize;
|
||||
let l = LoginParam::from_database(self, "").await?;
|
||||
let l2 = LoginParam::from_database(self, "configured_").await?;
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await? as usize;
|
||||
let real_msgs = message::get_real_msg_cnt(self).await as usize;
|
||||
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
|
||||
let contacts = Contact::get_real_cnt(self).await as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await;
|
||||
let contacts = Contact::get_real_cnt(self).await? as usize;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.await
|
||||
.get_raw_config_int("dbversion")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
|
||||
.await
|
||||
.query_get_value(sqlx::query("PRAGMA journal_mode;"))
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await;
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
|
||||
let prv_key_cnt: Option<isize> = self
|
||||
let prv_key_cnt = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await;
|
||||
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
|
||||
.await?;
|
||||
|
||||
let pub_key_cnt: Option<isize> = self
|
||||
let pub_key_cnt = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await;
|
||||
.count(sqlx::query("SELECT COUNT(*) FROM acpeerstates;"))
|
||||
.await?;
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await;
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "folders_configured")
|
||||
.await
|
||||
.get_raw_config_int("folders_configured")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let configured_sentbox_folder = self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_mvbox_folder = self
|
||||
.get_config(Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
|
||||
let mut res = get_info();
|
||||
|
||||
// insert values
|
||||
res.insert("bot", self.get_config_int(Config::Bot).await.to_string());
|
||||
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
|
||||
res.insert("number_of_chats", chats.to_string());
|
||||
res.insert("number_of_chat_messages", real_msgs.to_string());
|
||||
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
|
||||
@@ -344,7 +347,7 @@ impl Context {
|
||||
res.insert(
|
||||
"selfavatar",
|
||||
self.get_config(Config::Selfavatar)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
@@ -353,12 +356,12 @@ impl Context {
|
||||
res.insert(
|
||||
"fetch_existing_msgs",
|
||||
self.get_config_int(Config::FetchExistingMsgs)
|
||||
.await
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await.to_string(),
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
@@ -372,57 +375,51 @@ impl Context {
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await.to_string(),
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert(
|
||||
"private_key_count",
|
||||
prv_key_cnt.unwrap_or_default().to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"public_key_count",
|
||||
pub_key_cnt.unwrap_or_default().to_string(),
|
||||
);
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"webrtc_instance",
|
||||
self.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await.to_string(),
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_device_after",
|
||||
self.get_config_int(Config::DeleteDeviceAfter)
|
||||
.await
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_server_after",
|
||||
self.get_config_int(Config::DeleteServerAfter)
|
||||
.await
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_housekeeping",
|
||||
self.get_config_int(Config::LastHousekeeping)
|
||||
.await
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"scan_all_folders_debounce_secs",
|
||||
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
|
||||
.await
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
|
||||
res
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
|
||||
@@ -432,10 +429,10 @@ impl Context {
|
||||
/// Moreover, the number of returned messages
|
||||
/// can be used for a badge counter on the app icon.
|
||||
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let ret = self
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
concat!(
|
||||
.fetch(
|
||||
sqlx::query(concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
@@ -449,51 +446,38 @@ impl Context {
|
||||
" AND c.blocked=0",
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![MessageState::InFresh, time()],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for row in rows {
|
||||
ret.push(row?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
))
|
||||
.bind(MessageState::InFresh)
|
||||
.bind(time()),
|
||||
)
|
||||
.await?
|
||||
.map(|row| row?.try_get("id"))
|
||||
.collect::<sqlx::Result<_>>()
|
||||
.await?;
|
||||
Ok(ret)
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Searches for messages containing the query string.
|
||||
///
|
||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||
/// is `None` this searches messages from all chats.
|
||||
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: impl AsRef<str>) -> Vec<MsgId> {
|
||||
pub async fn search_msgs(
|
||||
&self,
|
||||
chat_id: Option<ChatId>,
|
||||
query: impl AsRef<str>,
|
||||
) -> Result<Vec<MsgId>> {
|
||||
let real_query = query.as_ref().trim();
|
||||
if real_query.is_empty() {
|
||||
return Vec::new();
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let str_like_in_text = format!("%{}%", real_query);
|
||||
let str_like_beg = 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)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
self.sql
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -502,13 +486,24 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND (txt LIKE ? OR ct.name LIKE ?)
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
paramsv![chat_id, str_like_in_text, str_like_beg],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.bind(chat_id)
|
||||
.bind(str_like_in_text)
|
||||
.bind(str_like_beg),
|
||||
)
|
||||
.await?
|
||||
.map(|row| {
|
||||
let row = row?;
|
||||
let id = row.try_get::<MsgId, _>("id")?;
|
||||
Ok(id)
|
||||
})
|
||||
.collect::<sqlx::Result<Vec<MsgId>>>()
|
||||
.await?
|
||||
} else {
|
||||
do_query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
self.sql
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -520,31 +515,45 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND (m.txt LIKE ? OR ct.name LIKE ?)
|
||||
ORDER BY m.timestamp DESC,m.id DESC;",
|
||||
paramsv![str_like_in_text, str_like_beg],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
)
|
||||
.bind(str_like_in_text)
|
||||
.bind(str_like_beg),
|
||||
)
|
||||
.await?
|
||||
.map(|row| {
|
||||
let row = row?;
|
||||
let id = row.try_get::<MsgId, _>("id")?;
|
||||
Ok(id)
|
||||
})
|
||||
.collect::<sqlx::Result<Vec<MsgId>>>()
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredInboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
||||
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
|
||||
Ok(inbox == Some(folder_name.as_ref().to_string()))
|
||||
}
|
||||
|
||||
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredSentboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||
|
||||
Ok(sentbox == Some(folder_name.as_ref().to_string()))
|
||||
}
|
||||
|
||||
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
|
||||
Ok(mvbox == Some(folder_name.as_ref().to_string()))
|
||||
}
|
||||
|
||||
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> bool {
|
||||
self.get_config(Config::ConfiguredSpamFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> Result<bool> {
|
||||
let is_spam = self.get_config(Config::ConfiguredSpamFolder).await?
|
||||
== Some(folder_name.as_ref().to_string());
|
||||
|
||||
Ok(is_spam)
|
||||
}
|
||||
|
||||
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
||||
@@ -620,7 +629,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await;
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::load_from_db(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -651,43 +660,49 @@ mod tests {
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await, 1);
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await, 3);
|
||||
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
@@ -696,7 +711,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
@@ -720,8 +735,9 @@ mod tests {
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![time() - 3600, bob.id],
|
||||
sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;")
|
||||
.bind(time() - 3600)
|
||||
.bind(bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -738,10 +754,7 @@ mod tests {
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=-2 WHERE id=?;",
|
||||
paramsv![bob.id],
|
||||
)
|
||||
.execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
@@ -811,7 +824,7 @@ mod tests {
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.get_info().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.get("database_dir").is_some());
|
||||
}
|
||||
|
||||
@@ -851,7 +864,7 @@ mod tests {
|
||||
"smtp_certificate_checks",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
@@ -860,7 +873,8 @@ mod tests {
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
format!("'{}' missing in get_info() output", key)
|
||||
"'{}' missing in get_info() output",
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -632,14 +632,6 @@ 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.
|
||||
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
|
||||
input
|
||||
@@ -1053,7 +1045,9 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// the message should be added only once a day - test that an hour later and nearly a day later
|
||||
@@ -1063,7 +1057,9 @@ mod tests {
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
maybe_warn_on_bad_time(
|
||||
@@ -1072,7 +1068,9 @@ mod tests {
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// next day, there should be another device message
|
||||
@@ -1085,7 +1083,9 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0));
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1115,7 +1115,9 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// do not repeat the warning every day ...
|
||||
@@ -1135,7 +1137,9 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let test_len = msgs.len();
|
||||
assert!(test_len == 1 || test_len == 2);
|
||||
|
||||
@@ -1150,7 +1154,9 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), test_len + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ struct Dehtml {
|
||||
/// Everything between <div name="quote"> and <div name="quoted-content"> is usually metadata
|
||||
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
|
||||
divs_since_quoted_content_div: u32,
|
||||
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
|
||||
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
|
||||
blockquotes_since_blockquote: u32,
|
||||
}
|
||||
|
||||
impl Dehtml {
|
||||
fn line_prefix(&self) -> &str {
|
||||
if self.divs_since_quoted_content_div > 0 {
|
||||
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
|
||||
"> "
|
||||
} else {
|
||||
""
|
||||
@@ -67,7 +70,7 @@ pub fn dehtml(buf: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn dehtml_quick_xml(buf: &str) -> String {
|
||||
fn dehtml_quick_xml(buf: &str) -> String {
|
||||
let buf = buf.trim().trim_start_matches("<!doctype html>");
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
@@ -76,6 +79,7 @@ pub fn dehtml_quick_xml(buf: &str) -> String {
|
||||
last_href: None,
|
||||
divs_since_quote_div: 0,
|
||||
divs_since_quoted_content_div: 0,
|
||||
blockquotes_since_blockquote: 0,
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
@@ -179,6 +183,7 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
dehtml.strbuilder += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +246,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
dehtml.strbuilder += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
113
src/e2ee.rs
113
src/e2ee.rs
@@ -26,9 +26,9 @@ pub struct EncryptHelper {
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await? {
|
||||
None => {
|
||||
bail!("addr not configured!");
|
||||
}
|
||||
@@ -209,29 +209,58 @@ pub async fn try_decrypt(
|
||||
Ok((out_mail, signatures))
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
||||
ensure!(
|
||||
mail.ctype.mimetype == "multipart/encrypted",
|
||||
"Not a multipart/encrypted message: {}",
|
||||
mail.ctype.mimetype
|
||||
);
|
||||
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
|
||||
///
|
||||
/// Returns `None` if the message is not a valid PGP/MIME message.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/encrypted" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
ensure!(
|
||||
first_part.ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
first_part.ctype,
|
||||
);
|
||||
|
||||
ensure!(
|
||||
second_part.ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
second_part.ctype
|
||||
);
|
||||
|
||||
Ok(second_part)
|
||||
if first_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& second_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(second_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
bail!("Invalid Autocrypt Level 1 Mime Parts")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload of a ["Mixed
|
||||
/// Up"][pgpmime-message-mangling] message.
|
||||
///
|
||||
/// According to [RFC 3156] encrypted messages should have
|
||||
/// `multipart/encrypted` MIME type and two parts, but Microsoft
|
||||
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
|
||||
/// structure by changing the type to `multipart/mixed` and prepending
|
||||
/// an empty part at the start.
|
||||
///
|
||||
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
|
||||
/// "Empty Message", so we don't check its contents at all, checking
|
||||
/// only for `text/plain` type.
|
||||
///
|
||||
/// Returns `None` if the message is not a "Mixed Up" message.
|
||||
///
|
||||
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
|
||||
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
|
||||
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/mixed" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part, third_part] = &mail.subparts[..] {
|
||||
if first_part.ctype.mimetype == "text/plain"
|
||||
&& second_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& third_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(third_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,12 +271,12 @@ async fn decrypt_if_autocrypt_message(
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail) {
|
||||
Err(_) => {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(res) => res,
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
|
||||
@@ -329,7 +358,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
"Failed to get self address, ",
|
||||
@@ -515,9 +544,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
let mut peerstates = Vec::new();
|
||||
peerstates.push((Some(peerstate), addr));
|
||||
peerstates
|
||||
vec![(Some(peerstate), addr)]
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -542,9 +569,31 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with missing peerstate
|
||||
let mut ps = Vec::new();
|
||||
ps.push((None, "bob@foo.bar"));
|
||||
let ps = vec![(None, "bob@foo.bar")];
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_up_mime() -> Result<()> {
|
||||
// "Mixed Up" mail as received when sending an encrypted
|
||||
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
|
||||
// Bridge.
|
||||
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
|
||||
let mail = mailparse::parse_mail(mixed_up_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_none());
|
||||
assert!(get_mixed_up_mime(&mail).is_some());
|
||||
|
||||
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
|
||||
//
|
||||
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
|
||||
// header although the repairing is done by the built-in
|
||||
// OpenPGP support, not Enigmail.
|
||||
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
|
||||
let mail = mailparse::parse_mail(repaired_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_some());
|
||||
assert!(get_mixed_up_mime(&mail).is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
226
src/ephemeral.rs
226
src/ephemeral.rs
@@ -61,9 +61,10 @@ use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{ensure, Error};
|
||||
use anyhow::{ensure, Context as _, Error};
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
|
||||
use crate::constants::{
|
||||
@@ -120,28 +121,41 @@ impl FromStr for Timer {
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for Timer {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => i64::from(*duration),
|
||||
});
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
impl sqlx::Type<sqlx::Sqlite> for Timer {
|
||||
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
||||
<i64 as sqlx::Type<_>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
|
||||
<i64 as sqlx::Type<_>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for Timer {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|value| {
|
||||
if value == 0 {
|
||||
Ok(Self::Disabled)
|
||||
} else if let Ok(duration) = u32::try_from(value) {
|
||||
Ok(Self::Enabled { duration })
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
||||
}
|
||||
})
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
||||
) -> sqlx::encode::IsNull {
|
||||
args.push(sqlx::sqlite::SqliteArgumentValue::Int64(
|
||||
self.to_u32() as i64
|
||||
));
|
||||
|
||||
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,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +164,8 @@ impl ChatId {
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self),
|
||||
)
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
@@ -172,10 +185,13 @@ impl ChatId {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats
|
||||
sqlx::query(
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
)
|
||||
.bind(timer)
|
||||
.bind(self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -214,44 +230,45 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
from_id: u32,
|
||||
) -> String {
|
||||
match timer {
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
0..=59 => {
|
||||
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
|
||||
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32)
|
||||
.await
|
||||
}
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await,
|
||||
61..=3599 => {
|
||||
stock_str::msg_ephemeral_timer_minutes(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
|
||||
from_id,
|
||||
from_id as u32,
|
||||
)
|
||||
.await
|
||||
}
|
||||
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
|
||||
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await,
|
||||
3601..=86399 => {
|
||||
stock_str::msg_ephemeral_timer_hours(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
|
||||
from_id,
|
||||
from_id as u32,
|
||||
)
|
||||
.await
|
||||
}
|
||||
86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
|
||||
86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await,
|
||||
86401..=604_799 => {
|
||||
stock_str::msg_ephemeral_timer_days(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
|
||||
from_id,
|
||||
from_id as u32,
|
||||
)
|
||||
.await
|
||||
}
|
||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
|
||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await,
|
||||
_ => {
|
||||
stock_str::msg_ephemeral_timer_weeks(
|
||||
context,
|
||||
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
|
||||
from_id,
|
||||
from_id as u32,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -261,33 +278,38 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
.query_get_value::<_, i64>(
|
||||
sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
Some(duration) => Timer::Enabled { duration },
|
||||
Some(duration) => Timer::Enabled {
|
||||
duration: u32::try_from(duration)?,
|
||||
},
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
sqlx::query(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||
)
|
||||
.bind(ephemeral_timestamp)
|
||||
.bind(ephemeral_timestamp)
|
||||
.bind(self),
|
||||
)
|
||||
.await?;
|
||||
schedule_ephemeral_task(context).await;
|
||||
@@ -308,20 +330,29 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
|
||||
let mut updated = context
|
||||
.sql
|
||||
.execute(
|
||||
// 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
|
||||
"UPDATE msgs \
|
||||
SET chat_id=?, txt='', subject='', txt_raw='', mime_headers='', from_id=0, to_id=0, param='' \
|
||||
WHERE \
|
||||
ephemeral_timestamp != 0 \
|
||||
AND ephemeral_timestamp <= ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||
sqlx::query(
|
||||
// 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
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
)
|
||||
.bind(DC_CHAT_ID_TRASH)
|
||||
.bind(time())
|
||||
.bind(DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
.context("update failed")?
|
||||
> 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)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -340,21 +371,22 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
|
||||
let rows_modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
sqlx::query(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.bind(DC_CHAT_ID_TRASH)
|
||||
.bind(threshold_timestamp)
|
||||
.bind(DC_CHAT_ID_LAST_SPECIAL)
|
||||
.bind(self_chat_id)
|
||||
.bind(device_chat_id),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("deleted update failed")?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
}
|
||||
@@ -376,14 +408,18 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
|
||||
pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let ephemeral_timestamp: Option<i64> = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timestamp \
|
||||
FROM msgs \
|
||||
WHERE ephemeral_timestamp != 0 \
|
||||
AND chat_id != ? \
|
||||
ORDER BY ephemeral_timestamp ASC \
|
||||
LIMIT 1",
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
.query_get_value(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT ephemeral_timestamp
|
||||
FROM msgs
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?
|
||||
ORDER BY ephemeral_timestamp ASC
|
||||
LIMIT 1;
|
||||
"#,
|
||||
)
|
||||
.bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -439,25 +475,34 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
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? {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
|
||||
context
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
.fetch_optional(
|
||||
sqlx::query(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
)
|
||||
.bind(threshold_timestamp)
|
||||
.bind(now),
|
||||
)
|
||||
.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
|
||||
@@ -473,17 +518,17 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
sqlx::query(
|
||||
"UPDATE msgs \
|
||||
SET ephemeral_timestamp = ? + ephemeral_timer \
|
||||
WHERE ephemeral_timer > 0 \
|
||||
AND ephemeral_timestamp = 0 \
|
||||
AND state NOT IN (?, ?, ?)",
|
||||
paramsv![
|
||||
time(),
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::OutDraft
|
||||
],
|
||||
)
|
||||
.bind(time())
|
||||
.bind(MessageState::InFresh)
|
||||
.bind(MessageState::InNoticed)
|
||||
.bind(MessageState::OutDraft),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -717,7 +762,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await;
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
@@ -730,12 +775,13 @@ mod tests {
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, 0);
|
||||
assert_eq!(msg.to_id, 0);
|
||||
assert!(msg.text.is_none_or_empty(), msg.text);
|
||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value(t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.await;
|
||||
assert!(rawtxt.is_none_or_empty(), rawtxt);
|
||||
.query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! # Error handling
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Out of Range")]
|
||||
pub struct OutOfRangeError;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_eq {
|
||||
($left:expr, $right:expr) => ({
|
||||
|
||||
48
src/html.rs
48
src/html.rs
@@ -13,12 +13,12 @@ use std::pin::Pin;
|
||||
use anyhow::Result;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::parse_message_id;
|
||||
use crate::param::Param::SendHtml;
|
||||
use crate::plaintext::PlainText;
|
||||
use crate::{context::Context, message};
|
||||
use lettre_email::PartBuilder;
|
||||
use mailparse::ParsedContentType;
|
||||
|
||||
@@ -244,32 +244,20 @@ impl MsgId {
|
||||
/// this is the case at least when `Message.has_html()` returns true
|
||||
/// (we do not save raw mime unconditionally in the database to save space).
|
||||
/// The corresponding ffi-function is `dc_get_msg_html()`.
|
||||
pub async fn get_html(self, context: &Context) -> Option<String> {
|
||||
let rawmime: Option<String> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT mime_headers FROM msgs WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await;
|
||||
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
|
||||
let rawmime = message::get_mime_headers(context, self).await?;
|
||||
|
||||
if let Some(rawmime) = rawmime {
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
|
||||
Err(err) => {
|
||||
warn!(context, "get_html: parser error: {}", err);
|
||||
None
|
||||
}
|
||||
Ok(parser) => Some(parser.html),
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
|
||||
Err(err) => {
|
||||
warn!(context, "get_html: parser error: {}", err);
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
warn!(context, "get_html: empty mime for {}", self);
|
||||
None
|
||||
Ok(parser) => Ok(Some(parser.html)),
|
||||
}
|
||||
} else {
|
||||
warn!(context, "get_html: no mime for {}", self);
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +427,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_get_html_empty() {
|
||||
let t = TestContext::new().await;
|
||||
let msg_id = MsgId::new_unset();
|
||||
assert!(msg_id.get_html(&t).await.is_none())
|
||||
assert!(msg_id.get_html(&t).await.unwrap().is_none())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -460,7 +448,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.get_text().unwrap().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// alice: create chat with bob and forward received html-message there
|
||||
@@ -474,7 +462,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().unwrap().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
@@ -487,7 +475,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().unwrap().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap();
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
@@ -517,7 +505,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// receive the message on another device
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db
|
||||
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
|
||||
alice.recv_msg(&msg).await;
|
||||
let chat = alice.get_self_chat().await;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
@@ -527,7 +515,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().unwrap().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
@@ -549,7 +537,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert_eq!(msg.get_text(), Some("plain text".to_string()));
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
|
||||
// let bob receive the message
|
||||
@@ -559,7 +547,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert_eq!(msg.get_text(), Some("plain text".to_string()));
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap();
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
}
|
||||
}
|
||||
|
||||
121
src/imap/mod.rs
121
src/imap/mod.rs
@@ -194,7 +194,7 @@ impl Imap {
|
||||
|
||||
let oauth2 = self.config.oauth2;
|
||||
|
||||
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::STARTTLS
|
||||
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::Starttls
|
||||
|| self.config.lp.security == Socket::Plain
|
||||
{
|
||||
let config = &mut self.config;
|
||||
@@ -203,7 +203,7 @@ impl Imap {
|
||||
|
||||
match Client::connect_insecure((imap_server, imap_port)).await {
|
||||
Ok(client) => {
|
||||
if config.lp.security == Socket::STARTTLS {
|
||||
if config.lp.security == Socket::Starttls {
|
||||
client.secure(imap_server, config.strict_tls).await
|
||||
} else {
|
||||
Ok(client)
|
||||
@@ -229,7 +229,7 @@ impl Imap {
|
||||
let addr: &str = config.addr.as_ref();
|
||||
|
||||
if let Some(token) =
|
||||
dc_get_oauth2_access_token(context, addr, imap_pw, true).await
|
||||
dc_get_oauth2_access_token(context, addr, imap_pw, true).await?
|
||||
{
|
||||
let auth = OAuth2 {
|
||||
user: imap_user.into(),
|
||||
@@ -267,7 +267,7 @@ impl Imap {
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
warn!(context, "{}", e);
|
||||
@@ -339,11 +339,11 @@ impl Imap {
|
||||
if self.is_connected() && !self.should_reconnect() {
|
||||
return Ok(());
|
||||
}
|
||||
if !context.is_configured().await {
|
||||
if !context.is_configured().await? {
|
||||
bail!("IMAP Connect without configured params");
|
||||
}
|
||||
|
||||
let param = LoginParam::from_database(context, "configured_").await;
|
||||
let param = LoginParam::from_database(context, "configured_").await?;
|
||||
// the trailing underscore is correct
|
||||
|
||||
if let Err(err) = self
|
||||
@@ -521,24 +521,29 @@ impl Imap {
|
||||
// Write collected UIDs to SQLite database.
|
||||
context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let conn2 = &mut conn;
|
||||
let tx = conn2.transaction()?;
|
||||
tx.execute(
|
||||
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
|
||||
params![folder],
|
||||
)?;
|
||||
for (uid, rfc724_mid) in &msg_ids {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
tx.execute(
|
||||
"UPDATE msgs \
|
||||
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
|
||||
params![folder, uid, rfc724_mid],
|
||||
)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?")
|
||||
.bind(&folder)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
for (uid, rfc724_mid) in &msg_ids {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
sqlx::query(
|
||||
"UPDATE msgs \
|
||||
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
|
||||
)
|
||||
.bind(&folder)
|
||||
.bind(uid)
|
||||
.bind(rfc724_mid)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -575,6 +580,15 @@ impl Imap {
|
||||
// new messages is only one command, just as a SELECT command)
|
||||
true
|
||||
} else if let Some(uid_next) = mailbox.uid_next {
|
||||
if uid_next < old_uid_next {
|
||||
warn!(
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {} from {} to {} without changing validity ({}), resyncing UIDs...",
|
||||
folder, old_uid_next, uid_next, new_uid_validity,
|
||||
);
|
||||
set_uid_next(context, folder, uid_next).await?;
|
||||
job::schedule_resync(context).await;
|
||||
}
|
||||
uid_next != old_uid_next // If uid_next changed, there are new emails
|
||||
} else {
|
||||
true // We have no uid_next and if in doubt, return true
|
||||
@@ -646,7 +660,7 @@ impl Imap {
|
||||
folder: S,
|
||||
fetch_existing_msgs: bool,
|
||||
) -> Result<bool> {
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await)
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
|
||||
let new_emails = self
|
||||
@@ -745,7 +759,7 @@ impl Imap {
|
||||
let session = self.session.as_mut().unwrap();
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
let search_command = format!("FROM \"{}\"", self_addr);
|
||||
@@ -1267,10 +1281,7 @@ impl Imap {
|
||||
context: &Context,
|
||||
create_mvbox: bool,
|
||||
) -> Result<()> {
|
||||
let folders_configured = context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured")
|
||||
.await;
|
||||
let folders_configured = context.sql.get_raw_config_int("folders_configured").await?;
|
||||
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1398,7 +1409,7 @@ impl Imap {
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
|
||||
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
|
||||
.await?;
|
||||
}
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
@@ -1510,7 +1521,7 @@ async fn precheck_imf(
|
||||
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
|
||||
);
|
||||
|
||||
let delete_server_after = context.get_config_delete_server_after().await;
|
||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
if msg_id
|
||||
@@ -1612,7 +1623,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let parent = get_prefetch_parent_message(context, headers).await?;
|
||||
let is_reply_to_chat_message = parent.is_some();
|
||||
if let Some(parent) = parent {
|
||||
if let Some(parent) = &parent {
|
||||
let chat = chat::Chat::load_from_db(context, parent.get_chat_id()).await?;
|
||||
if chat.typ == Chattype::Group {
|
||||
// This might be a group command, like removing a group member.
|
||||
@@ -1658,6 +1669,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
}
|
||||
ShowEmails::All => true,
|
||||
};
|
||||
|
||||
let should_download = (show && !blocked_contact) || maybe_ndn;
|
||||
Ok(should_download)
|
||||
}
|
||||
@@ -1720,9 +1732,15 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||
sqlx::query(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||
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?;
|
||||
Ok(())
|
||||
@@ -1736,10 +1754,7 @@ 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> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT uid_next FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
)
|
||||
.query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
@@ -1752,9 +1767,15 @@ pub(crate) async fn set_uidvalidity(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||
sqlx::query(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
|
||||
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?;
|
||||
Ok(())
|
||||
@@ -1763,26 +1784,28 @@ pub(crate) async fn set_uidvalidity(
|
||||
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Deprecated, use get_uid_next() and get_uidvalidity()
|
||||
pub async fn get_config_last_seen_uid<S: AsRef<str>>(context: &Context, folder: S) -> (u32, u32) {
|
||||
pub async fn get_config_last_seen_uid<S: AsRef<str>>(
|
||||
context: &Context,
|
||||
folder: S,
|
||||
) -> Result<(u32, u32)> {
|
||||
let key = format!("imap.mailbox.{}", folder.as_ref());
|
||||
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await? {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
(
|
||||
Ok((
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
)
|
||||
))
|
||||
} else {
|
||||
(0, 0)
|
||||
Ok((0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Imap {
|
||||
let elapsed_secs = last_scan.elapsed().as_secs();
|
||||
let debounce_secs = context
|
||||
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
if elapsed_secs < debounce_secs {
|
||||
return Ok(());
|
||||
@@ -95,8 +95,8 @@ async fn get_watched_folders(context: &Context) -> Vec<String> {
|
||||
(Config::InboxWatch, Config::ConfiguredInboxFolder),
|
||||
];
|
||||
for (watched, configured) in folder_watched_configured {
|
||||
if context.get_config_bool(*watched).await {
|
||||
if let Some(folder) = context.get_config(*configured).await {
|
||||
if context.get_config_bool(*watched).await.unwrap_or_default() {
|
||||
if let Ok(Some(folder)) = context.get_config(*configured).await {
|
||||
res.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
94
src/imex.rs
94
src/imex.rs
@@ -10,6 +10,7 @@ use async_std::{
|
||||
prelude::*,
|
||||
};
|
||||
use rand::{thread_rng, Rng};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::delete_and_reset_all_device_msgs;
|
||||
@@ -38,7 +39,7 @@ const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
|
||||
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum ImexMode {
|
||||
/// Export all private keys and all public keys of the user to the
|
||||
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
|
||||
@@ -170,8 +171,8 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
|
||||
match sql.open(context, &path, true).await {
|
||||
Ok(_) => {
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int(context, "backup_time")
|
||||
.await
|
||||
.get_raw_config_int("backup_time")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
@@ -271,7 +272,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
let private_key = SignedSecretKey::load_self(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
};
|
||||
@@ -333,7 +334,7 @@ pub fn create_setup_code(_context: &Context) -> String {
|
||||
}
|
||||
|
||||
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
|
||||
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
|
||||
if !context.sql.get_raw_config_bool("bcc_self").await? {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
// TODO: define this as a stockstring once the wording is settled.
|
||||
msg.text = Some(
|
||||
@@ -394,7 +395,7 @@ async fn set_self_key(
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
|
||||
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
@@ -404,7 +405,7 @@ async fn set_self_key(
|
||||
}
|
||||
};
|
||||
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr).await;
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
ensure!(self_addr.is_some(), "Missing self addr");
|
||||
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
|
||||
let keypair = pgp::KeyPair {
|
||||
@@ -493,7 +494,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!context.is_configured().await,
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
@@ -564,7 +565,7 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!context.is_configured().await,
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
@@ -594,9 +595,9 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
||||
|
||||
let total_files_cnt = context
|
||||
.sql
|
||||
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||
.await
|
||||
.unwrap_or_default() as usize;
|
||||
.count(sqlx::query("SELECT COUNT(*) FROM backup_blobs;"))
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
@@ -606,29 +607,25 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
|
||||
// consuming too much memory.
|
||||
let file_ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM backup_blobs ORDER BY id",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.fetch(sqlx::query("SELECT id FROM backup_blobs ORDER BY id"))
|
||||
.await?
|
||||
.map(|row| row?.try_get(0))
|
||||
.collect::<sqlx::Result<Vec<i64>>>()
|
||||
.await?;
|
||||
|
||||
let mut all_files_extracted = true;
|
||||
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
||||
// Load a single blob into memory
|
||||
let (file_name, file_blob) = context
|
||||
let row = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
paramsv![file_id],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
.fetch_one(
|
||||
sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
|
||||
.bind(file_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file_name: String = row.try_get(0)?;
|
||||
let file_blob: &[u8] = row.try_get(1)?;
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
@@ -646,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);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
dc_write_file(context, &path_filename, file_blob).await?;
|
||||
}
|
||||
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||
.execute(sqlx::query("DROP TABLE backup_blobs;"))
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
context.sql.execute(sqlx::query("VACUUM;")).await.ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
@@ -674,13 +671,13 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
.set_raw_config_int("backup_time", now as i32)
|
||||
.await?;
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("VACUUM;", paramsv![])
|
||||
.execute(sqlx::query("VACUUM;"))
|
||||
.await
|
||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
|
||||
|
||||
@@ -833,29 +830,26 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let mut export_errors = 0;
|
||||
|
||||
let keys = context
|
||||
let mut keys = context
|
||||
.sql
|
||||
.query_map(
|
||||
.fetch(sqlx::query(
|
||||
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let public_key_blob: Vec<u8> = row.get(1)?;
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||
let is_default: i32 = row.get(3)?;
|
||||
))
|
||||
.await?
|
||||
.map(|row| -> sqlx::Result<_> {
|
||||
let row = row?;
|
||||
let id = row.try_get(0)?;
|
||||
let public_key_blob: &[u8] = row.try_get(1)?;
|
||||
let public_key = SignedPublicKey::from_slice(public_key_blob);
|
||||
let private_key_blob: &[u8] = row.try_get(2)?;
|
||||
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))
|
||||
},
|
||||
|keys| {
|
||||
keys.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok((id, public_key, private_key, is_default))
|
||||
});
|
||||
|
||||
for (id, public_key, private_key, is_default) in keys {
|
||||
while let Some(parts) = keys.next().await {
|
||||
let (id, public_key, private_key, is_default) = parts?;
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
if let Ok(key) = public_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key)
|
||||
|
||||
369
src/job.rs
369
src/job.rs
@@ -7,10 +7,11 @@ use std::{fmt, time::Duration};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task::sleep;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
@@ -36,10 +37,8 @@ use crate::{scheduler::InterruptInfo, sql};
|
||||
const JOB_RETRIES: u32 = 17;
|
||||
|
||||
/// Thread IDs
|
||||
#[derive(
|
||||
Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(u32)]
|
||||
pub(crate) enum Thread {
|
||||
Unknown = 0,
|
||||
Imap = 100,
|
||||
@@ -76,19 +75,9 @@ impl Default for Thread {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum Action {
|
||||
Unknown = 0,
|
||||
|
||||
@@ -184,7 +173,7 @@ impl Job {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -203,26 +192,24 @@ impl Job {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
paramsv![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
self.param.to_string(),
|
||||
self.job_id as i32,
|
||||
],
|
||||
sqlx::query(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
)
|
||||
.bind(self.desired_timestamp)
|
||||
.bind(self.tries as i64)
|
||||
.bind(self.param.to_string())
|
||||
.bind(self.job_id as i32),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
self.added_timestamp,
|
||||
thread,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.param.to_string(),
|
||||
self.desired_timestamp
|
||||
]
|
||||
sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);")
|
||||
.bind(self.added_timestamp)
|
||||
.bind(thread)
|
||||
.bind(self.action)
|
||||
.bind(self.foreign_id)
|
||||
.bind(self.param.to_string())
|
||||
.bind(self.desired_timestamp)
|
||||
).await?;
|
||||
}
|
||||
|
||||
@@ -253,7 +240,7 @@ impl Job {
|
||||
let status = match smtp.send(context, recipients, message, job_id).await {
|
||||
Err(crate::smtp::send::Error::SendError(err)) => {
|
||||
// Remote error, retry later.
|
||||
warn!(context, "SMTP failed to send: {}", err);
|
||||
warn!(context, "SMTP failed to send: {:?}", err);
|
||||
self.pending_error = Some(err.to_string());
|
||||
|
||||
let res = match err {
|
||||
@@ -339,6 +326,12 @@ impl Job {
|
||||
error!(context, "SMTP job failed because SMTP has no transport");
|
||||
Status::Finished(Err(format_err!("SMTP has not transport")))
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "unable to load job: {}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
Ok(()) => {
|
||||
job_try!(success_cb().await);
|
||||
Status::Finished(Ok(()))
|
||||
@@ -387,11 +380,21 @@ impl Job {
|
||||
/* if there is a msg-id and it does not exist in the db, cancel sending.
|
||||
this happends if dc_delete_msgs() was called
|
||||
before the generated mime was sent out */
|
||||
if 0 != self.foreign_id && !message::exists(context, MsgId::new(self.foreign_id)).await {
|
||||
return Status::Finished(Err(format_err!(
|
||||
"Not sending Message {} as it was deleted",
|
||||
self.foreign_id
|
||||
)));
|
||||
if 0 != self.foreign_id {
|
||||
match message::exists(context, MsgId::new(self.foreign_id)).await {
|
||||
Ok(exists) => {
|
||||
if !exists {
|
||||
return Status::Finished(Err(format_err!(
|
||||
"Not sending Message {} as it was deleted",
|
||||
self.foreign_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to check message existence: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let foreign_id = self.foreign_id;
|
||||
@@ -399,7 +402,7 @@ impl Job {
|
||||
async move {
|
||||
// smtp success, update db ASAP, then delete smtp file
|
||||
if 0 != foreign_id {
|
||||
set_delivered(context, MsgId::new(foreign_id)).await;
|
||||
set_delivered(context, MsgId::new(foreign_id)).await?;
|
||||
}
|
||||
// now also delete the generated file
|
||||
dc_delete_file(context, filename).await;
|
||||
@@ -416,44 +419,38 @@ impl Job {
|
||||
contact_id: u32,
|
||||
) -> sql::Result<(Vec<u32>, Vec<String>)> {
|
||||
// Extract message IDs from job parameters
|
||||
let res: Vec<(u32, MsgId)> = context
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
|
||||
paramsv![contact_id, 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)
|
||||
},
|
||||
.fetch(
|
||||
sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
|
||||
.bind(contact_id)
|
||||
.bind(self.job_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Load corresponding RFC724 message IDs
|
||||
let mut job_ids = Vec::new();
|
||||
let mut rfc724_mids = Vec::new();
|
||||
for (job_id, msg_id) in res {
|
||||
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
|
||||
job_ids.push(job_id);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
let job_id: u32 = row.try_get(0)?;
|
||||
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))
|
||||
}
|
||||
|
||||
async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
|
||||
if !context.get_config_bool(Config::MdnsEnabled).await {
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if !mdns_enabled {
|
||||
// User has disabled MDNs after job scheduling but before
|
||||
// execution.
|
||||
return Status::Finished(Err(format_err!("MDNs are disabled")));
|
||||
@@ -539,7 +536,13 @@ impl Job {
|
||||
);
|
||||
return Status::Finished(Ok(()));
|
||||
}
|
||||
Ok(Some(config)) => context.get_config(config).await,
|
||||
Ok(Some(config)) => match context.get_config(config).await {
|
||||
Ok(folder) => folder,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load config: {}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
@@ -632,6 +635,7 @@ impl Job {
|
||||
// Hidden messages are similar to trashed, but are
|
||||
// related to some chat. We also delete their
|
||||
// database records.
|
||||
info!(context, "verbose (issue 2335): will delete from db");
|
||||
job_try!(msg.id.delete_from_db(context).await)
|
||||
} else {
|
||||
// Remove server UID from the database record.
|
||||
@@ -642,6 +646,7 @@ impl Job {
|
||||
// we remove UID to reduce the number of messages
|
||||
// pointing to the corresponding UID. Once the counter
|
||||
// reaches zero, we will remove the message.
|
||||
info!(context, "verbose (issue 2335): will unlink");
|
||||
job_try!(msg.id.unlink(context).await);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
@@ -657,7 +662,7 @@ impl Job {
|
||||
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
|
||||
/// and show them in the chat list.
|
||||
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if context.get_config_bool(Config::Bot).await {
|
||||
if job_try!(context.get_config_bool(Config::Bot).await) {
|
||||
return Status::Finished(Ok(())); // Bots don't want those messages
|
||||
}
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
@@ -669,13 +674,13 @@ impl Job {
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
|
||||
|
||||
if context.get_config_bool(Config::FetchExistingMsgs).await {
|
||||
if job_try!(context.get_config_bool(Config::FetchExistingMsgs).await) {
|
||||
for config in &[
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Config::ConfiguredInboxFolder,
|
||||
Config::ConfiguredSentboxFolder,
|
||||
] {
|
||||
if let Some(folder) = context.get_config(*config).await {
|
||||
if let Some(folder) = job_try!(context.get_config(*config).await) {
|
||||
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not fetch messages, retrying: {:#}", e);
|
||||
@@ -688,7 +693,7 @@ impl Job {
|
||||
// Make sure that if there now is a chat with a contact (created by an outgoing
|
||||
// message), then group contact requests from this contact should also be unblocked.
|
||||
// See https://github.com/deltachat/deltachat-core-rust/issues/2097.
|
||||
for item in chat::get_chat_msgs(context, ChatId::new(DC_CHAT_ID_DEADDROP), 0, None).await {
|
||||
for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = match Message::load_from_db(context, msg_id).await {
|
||||
Err(e) => {
|
||||
@@ -736,26 +741,21 @@ impl Job {
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, sentbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
|
||||
if let Some(sentbox_folder) = sentbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
|
||||
}
|
||||
|
||||
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, inbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
|
||||
if let Some(inbox_folder) = inbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
|
||||
}
|
||||
|
||||
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, mvbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
|
||||
if let Some(mvbox_folder) = mvbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
|
||||
}
|
||||
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
@@ -803,11 +803,13 @@ impl Job {
|
||||
// the name sent in the From field by the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& !msg.is_system_message()
|
||||
&& context.get_config_bool(Config::MdnsEnabled).await
|
||||
{
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if mdns_enabled {
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
@@ -820,50 +822,46 @@ impl Job {
|
||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||
.execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action))
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
),
|
||||
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
|
||||
)
|
||||
.await?;
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
);
|
||||
let mut query = sqlx::query(&q);
|
||||
for id in job_ids {
|
||||
query = query.bind(*id);
|
||||
}
|
||||
context.sql.execute(query).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
|
||||
.exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn set_delivered(context: &Context, msg_id: MsgId) {
|
||||
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT chat_id FROM msgs WHERE id=?",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.await
|
||||
.query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
|
||||
let mailbox = if let Some(m) = context.get_config(folder).await {
|
||||
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
|
||||
m
|
||||
} else {
|
||||
return;
|
||||
@@ -933,14 +931,14 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
|
||||
let from = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately.
|
||||
if context.get_config_bool(Config::BccSelf).await
|
||||
&& context.get_config_delete_server_after().await != Some(0)
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_delete_server_after().await? != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
@@ -954,7 +952,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
);
|
||||
set_delivered(context, msg_id).await;
|
||||
set_delivered(context, msg_id).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -1022,7 +1020,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.update_subject(context).await;
|
||||
|
||||
let job = create(Action::SendMsgToSmtp, msg_id.to_u32() as i32, param, 0)?;
|
||||
let job = create(Action::SendMsgToSmtp, msg_id.to_u32(), param, 0)?;
|
||||
|
||||
Ok(Some(job))
|
||||
}
|
||||
@@ -1034,6 +1032,7 @@ pub(crate) enum Connection<'a> {
|
||||
|
||||
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||
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(
|
||||
Action::DeleteMsgOnImap,
|
||||
msg_id.to_u32(),
|
||||
@@ -1143,7 +1142,7 @@ async fn perform_job_action(
|
||||
) -> Status {
|
||||
info!(
|
||||
context,
|
||||
"{} begin immediate try {} of job {}", &connection, tries, job
|
||||
"{} begin immediate try {} of job {:?} - verbose (issue 2335)", &connection, tries, job
|
||||
);
|
||||
|
||||
let try_res = match job.action {
|
||||
@@ -1200,13 +1199,13 @@ pub(crate) async fn schedule_resync(context: &Context) {
|
||||
}
|
||||
|
||||
/// Creates a job.
|
||||
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
|
||||
pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result<Job> {
|
||||
ensure!(
|
||||
action != Action::Unknown,
|
||||
"Invalid action passed to job_add"
|
||||
);
|
||||
|
||||
Ok(Job::new(action, foreign_id as u32, param, delay_seconds))
|
||||
Ok(Job::new(action, foreign_id, param, delay_seconds))
|
||||
}
|
||||
|
||||
/// Adds a job to the database, scheduling it.
|
||||
@@ -1245,7 +1244,13 @@ pub async fn add(context: &Context, job: Job) {
|
||||
}
|
||||
|
||||
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
|
||||
let last_time = context.get_config_i64(Config::LastHousekeeping).await;
|
||||
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
|
||||
Ok(last_time) => last_time,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load housekeeping config: {:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let next_time = last_time + (60 * 60 * 24);
|
||||
if next_time <= time() {
|
||||
@@ -1280,65 +1285,77 @@ pub(crate) async fn load_next(
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
query = r#"
|
||||
let get_query = || {
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
} else if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
query = r#"
|
||||
"#,
|
||||
)
|
||||
.bind(thread_i)
|
||||
.bind(msg_id)
|
||||
} else if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i, t];
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
// in the order of their backoff-times.
|
||||
query = r#"
|
||||
"#,
|
||||
)
|
||||
.bind(thread_i)
|
||||
.bind(t)
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
// in the order of their backoff-times.
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i];
|
||||
"#,
|
||||
)
|
||||
.bind(thread_i)
|
||||
}
|
||||
};
|
||||
|
||||
let job = loop {
|
||||
let job_res = context
|
||||
.sql
|
||||
.query_row_optional(query, params.clone(), |row| {
|
||||
let job = Job {
|
||||
job_id: row.get("id")?,
|
||||
action: row.get("action")?,
|
||||
foreign_id: row.get("foreign_id")?,
|
||||
desired_timestamp: row.get("desired_timestamp")?,
|
||||
added_timestamp: row.get("added_timestamp")?,
|
||||
tries: row.get("tries")?,
|
||||
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||
pending_error: None,
|
||||
};
|
||||
|
||||
Ok(job)
|
||||
})
|
||||
.await;
|
||||
.fetch_optional(get_query())
|
||||
.await
|
||||
.and_then(|row| {
|
||||
if let Some(row) = row {
|
||||
Ok(Some(Job {
|
||||
job_id: row.try_get("id")?,
|
||||
action: row.try_get("action")?,
|
||||
foreign_id: row.try_get("foreign_id")?,
|
||||
desired_timestamp: row.try_get("desired_timestamp")?,
|
||||
added_timestamp: row.try_get("added_timestamp")?,
|
||||
tries: row.try_get::<i64, _>("tries")? as u32,
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
pending_error: None,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
|
||||
match job_res {
|
||||
Ok(job) => break job,
|
||||
@@ -1349,15 +1366,18 @@ LIMIT 1;
|
||||
// TODO: improve by only doing a single query
|
||||
match context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.fetch_one(get_query())
|
||||
.await
|
||||
.and_then(|row| row.try_get::<i32, _>(0).map_err(Into::into))
|
||||
{
|
||||
Ok(id) => {
|
||||
context
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id))
|
||||
.await
|
||||
.ok();
|
||||
{
|
||||
warn!(context, "failed to delete job {}: {:?}", id, err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "failed to retrieve invalid job from DB: {}", err);
|
||||
@@ -1381,9 +1401,14 @@ LIMIT 1;
|
||||
.unwrap_or_default()
|
||||
.or(Some(job))
|
||||
} else {
|
||||
info!(context, "verbose (issue 2335): executing job normally");
|
||||
Some(job)
|
||||
}
|
||||
} 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)
|
||||
} else {
|
||||
load_housekeeping_job(context).await
|
||||
@@ -1399,22 +1424,22 @@ mod tests {
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
async fn insert_job(context: &Context, foreign_id: i64) {
|
||||
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
|
||||
let now = time();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO jobs
|
||||
sqlx::query(
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
Action::MoveMsg,
|
||||
foreign_id,
|
||||
Params::new().to_string(),
|
||||
now
|
||||
],
|
||||
)
|
||||
.bind(now)
|
||||
.bind(Thread::from(Action::MoveMsg))
|
||||
.bind(if valid { Action::MoveMsg as i32 } else { -1 })
|
||||
.bind(foreign_id)
|
||||
.bind(Params::new().to_string())
|
||||
.bind(now),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1426,7 +1451,7 @@ mod tests {
|
||||
// fails to load from the database instead of failing to load
|
||||
// all jobs.
|
||||
let t = TestContext::new().await;
|
||||
insert_job(&t, -1).await; // This can not be loaded into Job struct.
|
||||
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
@@ -1436,7 +1461,7 @@ mod tests {
|
||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||
assert!(jobs.unwrap().action == Action::Housekeeping);
|
||||
|
||||
insert_job(&t, 1).await;
|
||||
insert_job(&t, 1, true).await;
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
@@ -1450,7 +1475,7 @@ mod tests {
|
||||
async fn test_load_next_job_one() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
insert_job(&t, 1).await;
|
||||
insert_job(&t, 1, true).await;
|
||||
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
|
||||
75
src/key.rs
75
src/key.rs
@@ -9,6 +9,7 @@ use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
use sqlx::Row;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -41,6 +42,10 @@ pub enum Error {
|
||||
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||
#[error("no data provided")]
|
||||
Empty,
|
||||
#[error("db: {}", _0)]
|
||||
Sql(#[from] sqlx::Error),
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -118,24 +123,21 @@ impl DcKey for SignedPublicKey {
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
.fetch_optional(sqlx::query(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await
|
||||
))
|
||||
.await?
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
Some(row) => Self::from_slice(row.try_get(0)?),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,24 +165,21 @@ impl DcKey for SignedSecretKey {
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
.fetch_optional(sqlx::query(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await
|
||||
))
|
||||
.await?
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
Some(row) => Self::from_slice(row.try_get(0)?),
|
||||
None => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +220,7 @@ impl DcSecretKey for SignedSecretKey {
|
||||
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or(Error::NoConfiguredAddr)?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
@@ -229,26 +228,27 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
.fetch_optional(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.bind(addr.to_string()),
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
{
|
||||
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
Some(row) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
public: SignedPublicKey::from_slice(row.try_get(0)?)?,
|
||||
secret: SignedSecretKey::from_slice(row.try_get(1)?)?,
|
||||
}),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
None => {
|
||||
let start = std::time::SystemTime::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
let keypair =
|
||||
@@ -262,7 +262,6 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
);
|
||||
Ok(keypair)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,15 +319,16 @@ pub async fn store_self_keypair(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;")
|
||||
.bind(&public_key)
|
||||
.bind(&secret_key),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.execute(sqlx::query("UPDATE keypairs SET is_default=0;"))
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
}
|
||||
@@ -340,13 +340,18 @@ pub async fn store_self_keypair(
|
||||
let addr = keypair.addr.to_string();
|
||||
let t = time();
|
||||
|
||||
let params = paramsv![addr, is_default, public_key, secret_key, t];
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
sqlx::query(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
params,
|
||||
)
|
||||
.bind(addr)
|
||||
.bind(is_default)
|
||||
.bind(&public_key)
|
||||
.bind(&secret_key)
|
||||
.bind(t),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
|
||||
@@ -620,7 +625,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
let nrows = || async {
|
||||
ctx.sql
|
||||
.query_get_value::<u32>(&ctx, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -1,11 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow
|
||||
clippy::needless_borrow,
|
||||
unsafe_code
|
||||
)]
|
||||
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
||||
|
||||
@@ -13,16 +13,10 @@
|
||||
extern crate num_derive;
|
||||
#[macro_use]
|
||||
extern crate smallvec;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
extern crate strum;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
#[macro_use]
|
||||
|
||||
475
src/location.rs
475
src/location.rs
@@ -1,8 +1,11 @@
|
||||
//! Location handling
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{ensure, Error};
|
||||
use async_std::prelude::*;
|
||||
use bitflags::bitflags;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
@@ -198,15 +201,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
sqlx::query(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
.bind(if 0 != seconds { now } else { 0 })
|
||||
.bind(if 0 != seconds { now + seconds } else { 0 })
|
||||
.bind(chat_id),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -259,16 +262,17 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<Cha
|
||||
Some(chat_id) => context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
paramsv![chat_id, time()],
|
||||
sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;")
|
||||
.bind(chat_id)
|
||||
.bind(time()),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
None => context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;")
|
||||
.bind(time()),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
@@ -281,28 +285,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
}
|
||||
let mut continue_streaming = false;
|
||||
|
||||
if let Ok(chats) = context
|
||||
if let Ok(mut chats) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time()))
|
||||
.await
|
||||
.map(|rows| rows.map(|row| row?.try_get::<i32, _>(0)))
|
||||
{
|
||||
for chat_id in chats {
|
||||
while let Some(chat_id) = chats.next().await {
|
||||
let chat_id = match chat_id {
|
||||
Ok(id) => id,
|
||||
Err(_) => break,
|
||||
};
|
||||
if let Err(err) = context.sql.execute(
|
||||
sqlx::query(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
]
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);"
|
||||
)
|
||||
.bind(latitude)
|
||||
.bind(longitude)
|
||||
.bind(accuracy)
|
||||
.bind(time())
|
||||
.bind(chat_id)
|
||||
.bind(DC_CONTACT_ID_SELF)
|
||||
|
||||
).await {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
@@ -324,10 +329,11 @@ pub async fn get_range(
|
||||
contact_id: Option<u32>,
|
||||
timestamp_from: i64,
|
||||
mut timestamp_to: i64,
|
||||
) -> Vec<Location> {
|
||||
) -> Result<Vec<Location>, Error> {
|
||||
if timestamp_to == 0 {
|
||||
timestamp_to = time() + 10;
|
||||
}
|
||||
|
||||
let (disable_chat_id, chat_id) = match chat_id {
|
||||
Some(chat_id) => (0, chat_id),
|
||||
None => (1, ChatId::new(0)), // this ChatId is unused
|
||||
@@ -336,56 +342,52 @@ pub async fn get_range(
|
||||
Some(contact_id) => (0, contact_id),
|
||||
None => (1, 0), // this contact_id is unused
|
||||
};
|
||||
context
|
||||
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"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 \
|
||||
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
|
||||
AND (? OR l.from_id=?) \
|
||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
||||
paramsv![
|
||||
disable_chat_id,
|
||||
chat_id,
|
||||
disable_contact_id,
|
||||
contact_id as i32,
|
||||
timestamp_from,
|
||||
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)
|
||||
},
|
||||
)
|
||||
.bind(disable_chat_id)
|
||||
.bind(chat_id)
|
||||
.bind(disable_contact_id)
|
||||
.bind(contact_id as i64)
|
||||
.bind(timestamp_from)
|
||||
.bind(timestamp_to),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.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?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
fn is_marker(txt: &str) -> bool {
|
||||
@@ -401,7 +403,7 @@ fn is_marker(txt: &str) -> bool {
|
||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
.execute(sqlx::query("DELETE FROM locations;"))
|
||||
.await?;
|
||||
context.emit_event(EventType::LocationChanged(None));
|
||||
Ok(())
|
||||
@@ -412,19 +414,23 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||
paramsv![chat_id], |row| {
|
||||
let send_begin: i64 = row.get(0)?;
|
||||
let send_until: i64 = row.get(1)?;
|
||||
let last_sent: i64 = row.get(2)?;
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = {
|
||||
let row = context.sql.fetch_one(
|
||||
sqlx::query(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;"
|
||||
)
|
||||
.bind(chat_id)
|
||||
).await?;
|
||||
|
||||
Ok((send_begin, send_until, last_sent))
|
||||
})
|
||||
.await?;
|
||||
let send_begin: i64 = row.try_get(0)?;
|
||||
let send_until: i64 = row.try_get(1)?;
|
||||
let last_sent: i64 = row.try_get(2)?;
|
||||
|
||||
(send_begin, send_until, last_sent)
|
||||
};
|
||||
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
@@ -435,40 +441,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
self_addr,
|
||||
);
|
||||
|
||||
context.sql.query_map(
|
||||
"SELECT id, latitude, longitude, accuracy, timestamp \
|
||||
let mut rows = context.sql.fetch(
|
||||
sqlx::query(
|
||||
"SELECT id, latitude, longitude, accuracy, timestamp \
|
||||
FROM locations WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
||||
AND independent=0 \
|
||||
GROUP 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(())
|
||||
}
|
||||
ORDER BY timestamp;"
|
||||
)
|
||||
.bind(DC_CONTACT_ID_SELF)
|
||||
.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>";
|
||||
}
|
||||
|
||||
@@ -509,8 +516,9 @@ pub async fn set_kml_sent_timestamp(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
paramsv![timestamp, chat_id],
|
||||
sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;")
|
||||
.bind(timestamp)
|
||||
.bind(chat_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -524,8 +532,9 @@ pub async fn set_msg_location_id(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
paramsv![location_id, msg_id],
|
||||
sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;")
|
||||
.bind(location_id)
|
||||
.bind(msg_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -544,6 +553,11 @@ pub async fn save(
|
||||
let mut newest_timestamp = 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\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
VALUES (?,?,?,?,?,?,?);";
|
||||
|
||||
for location in locations {
|
||||
let &Location {
|
||||
timestamp,
|
||||
@@ -552,53 +566,42 @@ pub async fn save(
|
||||
accuracy,
|
||||
..
|
||||
} = location;
|
||||
let (loc_id, ts) = context
|
||||
let exists = context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let mut stmt_test = conn
|
||||
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||
let mut stmt_insert = conn.prepare_cached(
|
||||
"INSERT INTO locations\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
VALUES (?,?,?,?,?,?,?);",
|
||||
)?;
|
||||
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
timestamp,
|
||||
contact_id as i32,
|
||||
chat_id,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
independent,
|
||||
])?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = crate::sql::get_rowid2(
|
||||
&mut conn,
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
contact_id as i32,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((newest_location_id, newest_timestamp))
|
||||
})
|
||||
.exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id))
|
||||
.await?;
|
||||
newest_timestamp = ts;
|
||||
newest_location_id = loc_id;
|
||||
if independent || !exists {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
sqlx::query(stmt_insert)
|
||||
.bind(timestamp)
|
||||
.bind(contact_id)
|
||||
.bind(chat_id)
|
||||
.bind(latitude)
|
||||
.bind(longitude)
|
||||
.bind(accuracy)
|
||||
.bind(independent),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = context
|
||||
.sql
|
||||
.get_rowid2(
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
contact_id as i64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
Ok(u32::try_from(newest_location_id)?)
|
||||
}
|
||||
|
||||
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
|
||||
@@ -611,15 +614,21 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
FROM chats \
|
||||
WHERE locations_send_until>?;",
|
||||
paramsv![now],
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let locations_send_begin: i64 = row.get(1)?;
|
||||
let locations_last_sent: i64 = row.get(2)?;
|
||||
)
|
||||
.bind(now),
|
||||
)
|
||||
.await
|
||||
.map(|rows| {
|
||||
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;
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
@@ -628,64 +637,55 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if rows.is_ok() {
|
||||
let msgs = context
|
||||
.sql
|
||||
.with_conn(move |conn| {
|
||||
let rows = rows.unwrap();
|
||||
|
||||
let mut stmt_locations = conn.prepare_cached(
|
||||
"SELECT id \
|
||||
FROM locations \
|
||||
WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND timestamp>? \
|
||||
AND independent=0 \
|
||||
ORDER BY timestamp;",
|
||||
)?;
|
||||
|
||||
let mut msgs = Vec::new();
|
||||
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
|
||||
if !stmt_locations
|
||||
.exists(paramsv![
|
||||
DC_CONTACT_ID_SELF,
|
||||
*locations_send_begin,
|
||||
*locations_last_sent,
|
||||
])
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
msgs.push((*chat_id, msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msgs)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
.filter_map(|v| v.transpose())
|
||||
});
|
||||
|
||||
let stmt = "SELECT COUNT(*) \
|
||||
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();
|
||||
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.
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
for (chat_id, mut msg) in msgs.into_iter() {
|
||||
// TODO: better error handling
|
||||
@@ -711,16 +711,16 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
|
||||
let chat_id = ChatId::new(job.foreign_id);
|
||||
|
||||
let (send_begin, send_until) = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
let (send_begin, send_until) = job_try!(context
|
||||
.sql
|
||||
.fetch_one(
|
||||
sqlx::query(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
);
|
||||
.bind(chat_id)
|
||||
)
|
||||
.await
|
||||
.and_then(|row| { Ok((row.try_get::<i64, _>(0)?, row.try_get::<i64, _>(1)?)) }));
|
||||
|
||||
if !(send_begin != 0 && time() <= send_until) {
|
||||
// still streaming -
|
||||
@@ -728,10 +728,19 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
|
||||
if !(send_begin == 0 && send_until == 0) {
|
||||
// not streaming, device-message already sent
|
||||
job_try!(context.sql.execute(
|
||||
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
).await);
|
||||
job_try!(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
sqlx::query(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=0, locations_send_until=0 \
|
||||
WHERE id=?"
|
||||
)
|
||||
.bind(chat_id)
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, stock_str).await;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::{context::Context, provider::Socket};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CertificateChecks {
|
||||
/// Same as AcceptInvalidCertificates unless overridden by
|
||||
@@ -54,91 +54,85 @@ pub struct LoginParam {
|
||||
|
||||
impl LoginParam {
|
||||
/// Read the login parameters from the database.
|
||||
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
|
||||
pub async fn from_database(
|
||||
context: &Context,
|
||||
prefix: impl AsRef<str>,
|
||||
) -> crate::sql::Result<Self> {
|
||||
let prefix = prefix.as_ref();
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
let addr = sql
|
||||
.get_raw_config(context, key)
|
||||
.await
|
||||
.get_raw_config(key)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
let mail_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
let mail_security = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.get_raw_config_int(key)
|
||||
.await?
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let imap_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
let send_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
let send_security = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.get_raw_config_int(key)
|
||||
.await?
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
let server_flags = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
|
||||
let key = format!("{}provider", prefix);
|
||||
let provider = sql
|
||||
.get_raw_config(context, key)
|
||||
.await
|
||||
.get_raw_config(key)
|
||||
.await?
|
||||
.and_then(|provider_id| get_provider_by_id(&provider_id));
|
||||
|
||||
LoginParam {
|
||||
Ok(LoginParam {
|
||||
addr,
|
||||
imap: ServerLoginParam {
|
||||
server: mail_server,
|
||||
@@ -158,7 +152,7 @@ impl LoginParam {
|
||||
},
|
||||
provider,
|
||||
server_flags,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Save this loginparam to the database.
|
||||
@@ -171,63 +165,54 @@ impl LoginParam {
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.addr)).await?;
|
||||
sql.set_raw_config(key, Some(&self.addr)).await?;
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.imap.server))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.imap.server)).await?;
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap.port as i32)
|
||||
.await?;
|
||||
sql.set_raw_config_int(key, self.imap.port as i32).await?;
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.imap.user))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.imap.user)).await?;
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.imap.password))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.imap.password)).await?;
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap.security as i32)
|
||||
sql.set_raw_config_int(key, self.imap.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
|
||||
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.server))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp.port as i32)
|
||||
.await?;
|
||||
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.user))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.password))
|
||||
.await?;
|
||||
sql.set_raw_config(key, Some(&self.smtp.password)).await?;
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp.security as i32)
|
||||
sql.set_raw_config_int(key, self.smtp.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
|
||||
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
sql.set_raw_config_int(context, key, self.server_flags)
|
||||
.await?;
|
||||
sql.set_raw_config_int(key, self.server_flags).await?;
|
||||
|
||||
if let Some(provider) = self.provider {
|
||||
let key = format!("{}provider", prefix);
|
||||
sql.set_raw_config(context, key, Some(provider.id)).await?;
|
||||
sql.set_raw_config(key, Some(provider.id)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
14
src/lot.rs
14
src/lot.rs
@@ -1,5 +1,3 @@
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
|
||||
/// An object containing a set of values.
|
||||
@@ -22,9 +20,7 @@ pub struct Lot {
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
@@ -68,10 +64,8 @@ impl Lot {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
@@ -87,7 +81,7 @@ pub enum LotState {
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMissmatch = 220,
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// test1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
652
src/message.rs
652
src/message.rs
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,11 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Error};
|
||||
use async_std::prelude::*;
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::config::Config;
|
||||
@@ -20,11 +28,6 @@ use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{bail, ensure, format_err, Error};
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
use std::convert::TryInto;
|
||||
|
||||
// attachments of 25 mb brutto should work on the majority of providers
|
||||
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
|
||||
@@ -36,7 +39,7 @@ const UPPER_LIMIT_FILE_SIZE: u64 = 49 * 1024 * 1024 / 4 * 3;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Loaded {
|
||||
Message { chat: Chat },
|
||||
MDN { additional_msg_ids: Vec<String> },
|
||||
Mdn { additional_msg_ids: Vec<String> },
|
||||
}
|
||||
|
||||
/// Helper to construct mime messages.
|
||||
@@ -92,12 +95,12 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let from_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let config_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let (from_displayname, sender_displayname) =
|
||||
if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) {
|
||||
@@ -112,52 +115,42 @@ impl<'a> MimeFactory<'a> {
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else {
|
||||
context
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
.fetch(
|
||||
sqlx::query(
|
||||
"SELECT c.authname, c.addr \
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
paramsv![msg.chat_id],
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (authname, addr) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.bind(msg.chat_id),
|
||||
)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
let (in_reply_to, references) = context
|
||||
let row = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
paramsv![msg.id],
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
|
||||
Ok((
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
))
|
||||
},
|
||||
.fetch_one(
|
||||
sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?")
|
||||
.bind(msg.id),
|
||||
)
|
||||
.await
|
||||
.context("Can't get mime_in_reply_to, mime_references")?;
|
||||
.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 factory = MimeFactory {
|
||||
@@ -166,7 +159,7 @@ impl<'a> MimeFactory<'a> {
|
||||
sender_displayname,
|
||||
selfstatus: context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or(default_str),
|
||||
recipients,
|
||||
timestamp: msg.timestamp_sort,
|
||||
@@ -191,16 +184,16 @@ impl<'a> MimeFactory<'a> {
|
||||
let contact = Contact::load_from_db(context, msg.from_id).await?;
|
||||
let from_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let from_displayname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let default_str = stock_str::status_line(context).await;
|
||||
let selfstatus = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.await?
|
||||
.unwrap_or(default_str);
|
||||
let timestamp = dc_create_smeared_timestamp(context).await;
|
||||
|
||||
@@ -214,7 +207,7 @@ impl<'a> MimeFactory<'a> {
|
||||
contact.get_addr().to_string(),
|
||||
)],
|
||||
timestamp,
|
||||
loaded: Loaded::MDN { additional_msg_ids },
|
||||
loaded: Loaded::Mdn { additional_msg_ids },
|
||||
msg,
|
||||
in_reply_to: String::default(),
|
||||
references: String::default(),
|
||||
@@ -232,7 +225,7 @@ impl<'a> MimeFactory<'a> {
|
||||
) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
@@ -265,7 +258,7 @@ impl<'a> MimeFactory<'a> {
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Loaded::MDN { .. } => false,
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +271,7 @@ impl<'a> MimeFactory<'a> {
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
}
|
||||
}
|
||||
Loaded::MDN { .. } => PeerstateVerifiedStatus::Unverified,
|
||||
Loaded::Mdn { .. } => PeerstateVerifiedStatus::Unverified,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +287,7 @@ impl<'a> MimeFactory<'a> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Loaded::MDN { .. } => true,
|
||||
Loaded::Mdn { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,22 +298,22 @@ impl<'a> MimeFactory<'a> {
|
||||
.param
|
||||
.get_bool(Param::SkipAutocrypt)
|
||||
.unwrap_or_default(),
|
||||
Loaded::MDN { .. } => true,
|
||||
Loaded::Mdn { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_do_gossip(&self, context: &Context) -> bool {
|
||||
async fn should_do_gossip(&self, context: &Context) -> Result<bool, Error> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
// beside key- and member-changes, force re-gossip every 48 hours
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await;
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
|
||||
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
|
||||
return true;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup)
|
||||
}
|
||||
|
||||
self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
|
||||
}
|
||||
Loaded::MDN { .. } => false,
|
||||
Loaded::Mdn { .. } => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,14 +343,14 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
None
|
||||
}
|
||||
Loaded::MDN { .. } => None,
|
||||
Loaded::Mdn { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn subject_str(&self, context: &Context) -> anyhow::Result<String> {
|
||||
let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject);
|
||||
|
||||
Ok(match self.loaded {
|
||||
let subject = match self.loaded {
|
||||
Loaded::Message { ref chat } => {
|
||||
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
return Ok(stock_str::ac_setup_msg_subject(context).await);
|
||||
@@ -387,16 +380,18 @@ impl<'a> MimeFactory<'a> {
|
||||
if let Some(last_subject) = parent_subject {
|
||||
format!("Re: {}", remove_subject_prefix(last_subject))
|
||||
} else {
|
||||
let self_name = match context.get_config(Config::Displayname).await {
|
||||
let self_name = match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await.unwrap_or_default(),
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
};
|
||||
|
||||
stock_str::subject_for_new_contact(context, self_name).await
|
||||
}
|
||||
}
|
||||
Loaded::MDN { .. } => stock_str::read_rcpt(context).await,
|
||||
})
|
||||
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
|
||||
};
|
||||
|
||||
Ok(subject)
|
||||
}
|
||||
|
||||
pub fn recipients(&self) -> Vec<String> {
|
||||
@@ -456,7 +451,7 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
unprotected_headers.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(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-replied".to_string(),
|
||||
@@ -502,7 +497,7 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let rfc724_mid = match self.loaded {
|
||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
|
||||
@@ -539,7 +534,7 @@ impl<'a> MimeFactory<'a> {
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Loaded::MDN { .. } => (self.render_mdn(context).await?, Vec::new()),
|
||||
Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()),
|
||||
};
|
||||
|
||||
let peerstates = self.peerstates_for_recipients(context).await?;
|
||||
@@ -567,7 +562,7 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
// 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()) {
|
||||
if peerstate.peek_key(min_verified).is_some() {
|
||||
if let Some(header) = peerstate.render_gossip_header(min_verified) {
|
||||
@@ -709,7 +704,7 @@ impl<'a> MimeFactory<'a> {
|
||||
) -> Result<(PartBuilder, Vec<PartBuilder>), Error> {
|
||||
let chat = match &self.loaded {
|
||||
Loaded::Message { chat } => chat,
|
||||
Loaded::MDN { .. } => bail!("Attempt to render MDN as a message"),
|
||||
Loaded::Mdn { .. } => bail!("Attempt to render MDN as a message"),
|
||||
};
|
||||
let command = self.msg.param.get_cmd();
|
||||
let mut placeholdertext = None;
|
||||
@@ -966,7 +961,9 @@ impl<'a> MimeFactory<'a> {
|
||||
// for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message.
|
||||
if self.msg.has_html() {
|
||||
let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) {
|
||||
MsgId::new(orig_msg_id.try_into()?).get_html(context).await
|
||||
MsgId::new(orig_msg_id.try_into()?)
|
||||
.get_html(context)
|
||||
.await?
|
||||
} else {
|
||||
self.msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
};
|
||||
@@ -1009,7 +1006,7 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok((part, filename)) => {
|
||||
parts.push(part);
|
||||
@@ -1040,7 +1037,7 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let additional_msg_ids = match &self.loaded {
|
||||
Loaded::Message { .. } => bail!("Attempt to render a message as MDN"),
|
||||
Loaded::MDN {
|
||||
Loaded::Mdn {
|
||||
additional_msg_ids, ..
|
||||
} => additional_msg_ids,
|
||||
};
|
||||
@@ -1137,14 +1134,14 @@ async fn build_body_file(
|
||||
// etc.
|
||||
let filename_to_send: String = match msg.viewtype {
|
||||
Viewtype::Voice => chrono::Utc
|
||||
.timestamp(msg.timestamp_sort as i64, 0)
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.format(&format!("voice-message_%Y-%m-%d_%H-%M-%S.{}", &suffix))
|
||||
.to_string(),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"{}.{}",
|
||||
if base_name.is_empty() {
|
||||
chrono::Utc
|
||||
.timestamp(msg.timestamp_sort as i64, 0)
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.format("image_%Y-%m-%d_%H-%M-%S")
|
||||
.to_string()
|
||||
} else {
|
||||
@@ -1155,7 +1152,7 @@ async fn build_body_file(
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp(msg.timestamp_sort as i64, 0)
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.format("%Y-%m-%d_%H-%M-%S")
|
||||
.to_string(),
|
||||
&suffix
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::pin::Pin;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use charset::Charset;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -103,10 +102,8 @@ pub(crate) enum MailinglistType {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum SystemMessage {
|
||||
Unknown = 0,
|
||||
GroupNameChanged = 2,
|
||||
@@ -1215,10 +1212,16 @@ impl MimeMessage {
|
||||
for original_message_id in
|
||||
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
|
||||
{
|
||||
if let Some((chat_id, msg_id)) =
|
||||
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
|
||||
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
|
||||
.await
|
||||
{
|
||||
context.emit_event(EventType::MsgRead { chat_id, msg_id });
|
||||
Ok(Some((chat_id, msg_id))) => {
|
||||
context.emit_event(EventType::MsgRead { chat_id, msg_id });
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to handle_mdn: {:#}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1245,9 +1248,8 @@ impl MimeMessage {
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![field],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -1918,8 +1920,9 @@ mod tests {
|
||||
.ctx
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
|
||||
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
|
||||
sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)")
|
||||
.bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org")
|
||||
.bind(timestamp),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
@@ -2741,6 +2744,19 @@ On 2020-10-25, Bob wrote:
|
||||
assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_allinkl_blockquote() {
|
||||
// all-inkl.com puts quotes into `<blockquote> </blockquote>`.
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/allinkl-quote.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
|
||||
assert!(mimeparser.parts[0].msg.starts_with("It's 1.0."));
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
|
||||
"What's the version?"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_subj_to_multimedia_msg() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
110
src/oauth2.rs
110
src/oauth2.rs
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -58,11 +59,7 @@ pub async fn dc_get_oauth2_url(
|
||||
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
||||
if context
|
||||
.sql
|
||||
.set_raw_config(
|
||||
context,
|
||||
"oauth2_pending_redirect_uri",
|
||||
Some(redirect_uri.as_ref()),
|
||||
)
|
||||
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri.as_ref()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@@ -82,31 +79,25 @@ pub async fn dc_get_oauth2_access_token(
|
||||
addr: impl AsRef<str>,
|
||||
code: impl AsRef<str>,
|
||||
regenerate: bool,
|
||||
) -> Option<String> {
|
||||
) -> Result<Option<String>> {
|
||||
if let Some(oauth2) = Oauth2::from_address(addr).await {
|
||||
let lock = context.oauth2_mutex.lock().await;
|
||||
|
||||
// read generated token
|
||||
if !regenerate && !is_expired(context).await {
|
||||
let access_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_access_token")
|
||||
.await;
|
||||
if !regenerate && !is_expired(context).await? {
|
||||
let access_token = context.sql.get_raw_config("oauth2_access_token").await?;
|
||||
if access_token.is_some() {
|
||||
// success
|
||||
return access_token;
|
||||
return Ok(access_token);
|
||||
}
|
||||
}
|
||||
|
||||
// generate new token: build & call auth url
|
||||
let refresh_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token")
|
||||
.await;
|
||||
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await?;
|
||||
let refresh_token_for = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token_for")
|
||||
.await
|
||||
.get_raw_config("oauth2_refresh_token_for")
|
||||
.await?
|
||||
.unwrap_or_else(|| "unset".into());
|
||||
|
||||
let (redirect_uri, token_url, update_redirect_uri_on_success) =
|
||||
@@ -115,8 +106,8 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_pending_redirect_uri")
|
||||
.await
|
||||
.get_raw_config("oauth2_pending_redirect_uri")
|
||||
.await?
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.init_token,
|
||||
true,
|
||||
@@ -129,8 +120,8 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_redirect_uri")
|
||||
.await
|
||||
.get_raw_config("oauth2_redirect_uri")
|
||||
.await?
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.refresh_token,
|
||||
false,
|
||||
@@ -166,7 +157,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
let mut req = surf::post(post_url).build();
|
||||
if let Err(err) = req.body_form(&post_param) {
|
||||
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let client = surf::Client::new();
|
||||
@@ -176,7 +167,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
|
||||
);
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// update refresh_token if given, typically on the first round, but we update it later as well.
|
||||
@@ -184,14 +175,12 @@ pub async fn dc_get_oauth2_access_token(
|
||||
if let Some(ref token) = response.refresh_token {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_refresh_token", Some(token))
|
||||
.await
|
||||
.ok();
|
||||
.set_raw_config("oauth2_refresh_token", Some(token))
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
|
||||
.await
|
||||
.ok();
|
||||
.set_raw_config("oauth2_refresh_token_for", Some(code.as_ref()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// after that, save the access token.
|
||||
@@ -199,9 +188,8 @@ pub async fn dc_get_oauth2_access_token(
|
||||
if let Some(ref token) = response.access_token {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_access_token", Some(token))
|
||||
.await
|
||||
.ok();
|
||||
.set_raw_config("oauth2_access_token", Some(token))
|
||||
.await?;
|
||||
let expires_in = response
|
||||
.expires_in
|
||||
// refresh a bit before
|
||||
@@ -209,16 +197,14 @@ pub async fn dc_get_oauth2_access_token(
|
||||
.unwrap_or_else(|| 0);
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
|
||||
.await
|
||||
.ok();
|
||||
.set_raw_config_int64("oauth2_timestamp_expires", expires_in)
|
||||
.await?;
|
||||
|
||||
if update_redirect_uri_on_success {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
|
||||
.await
|
||||
.ok();
|
||||
.set_raw_config("oauth2_redirect_uri", Some(redirect_uri.as_ref()))
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Failed to find OAuth2 access token");
|
||||
@@ -226,11 +212,11 @@ pub async fn dc_get_oauth2_access_token(
|
||||
|
||||
drop(lock);
|
||||
|
||||
response.access_token
|
||||
Ok(response.access_token)
|
||||
} else {
|
||||
warn!(context, "Internal OAuth2 error: 2");
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,27 +224,33 @@ pub async fn dc_get_oauth2_addr(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
code: impl AsRef<str>,
|
||||
) -> Option<String> {
|
||||
let oauth2 = Oauth2::from_address(addr.as_ref()).await?;
|
||||
oauth2.get_userinfo?;
|
||||
) -> Result<Option<String>> {
|
||||
let oauth2 = match Oauth2::from_address(addr.as_ref()).await {
|
||||
Some(o) => o,
|
||||
None => return Ok(None),
|
||||
};
|
||||
if oauth2.get_userinfo.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(access_token) =
|
||||
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
|
||||
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await?
|
||||
{
|
||||
let addr_out = oauth2.get_addr(context, access_token).await;
|
||||
if addr_out.is_none() {
|
||||
// regenerate
|
||||
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
|
||||
if let Some(access_token) =
|
||||
dc_get_oauth2_access_token(context, addr, code, true).await?
|
||||
{
|
||||
oauth2.get_addr(context, access_token).await
|
||||
Ok(oauth2.get_addr(context, access_token).await)
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
addr_out
|
||||
Ok(addr_out)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,21 +309,21 @@ impl Oauth2 {
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_expired(context: &Context) -> bool {
|
||||
async fn is_expired(context: &Context) -> Result<bool, crate::sql::Error> {
|
||||
let expire_timestamp = context
|
||||
.sql
|
||||
.get_raw_config_int64(context, "oauth2_timestamp_expires")
|
||||
.await
|
||||
.get_raw_config_int64("oauth2_timestamp_expires")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if expire_timestamp <= 0 {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
if expire_timestamp > time() {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
true
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn replace_in_uri(uri: impl AsRef<str>, key: impl AsRef<str>, value: impl AsRef<str>) -> String {
|
||||
@@ -399,7 +391,7 @@ mod tests {
|
||||
let ctx = TestContext::new().await;
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap();
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
@@ -419,7 +411,9 @@ mod tests {
|
||||
let ctx = TestContext::new().await;
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false)
|
||||
.await
|
||||
.unwrap();
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ impl Params {
|
||||
|
||||
pub fn get_msg_id(&self) -> Option<MsgId> {
|
||||
self.get(Param::MsgId)
|
||||
.and_then(|x| x.parse::<u32>().ok())
|
||||
.and_then(|x| x.parse().ok())
|
||||
.map(MsgId::new)
|
||||
}
|
||||
|
||||
|
||||
223
src/peerstate.rs
223
src/peerstate.rs
@@ -5,6 +5,7 @@ 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::chat;
|
||||
@@ -139,12 +140,15 @@ impl Peerstate {
|
||||
}
|
||||
|
||||
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
|
||||
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, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE addr=? COLLATE NOCASE;";
|
||||
Self::from_stmt(context, query, paramsv![addr]).await
|
||||
WHERE addr=? COLLATE NOCASE;",
|
||||
)
|
||||
.bind(addr);
|
||||
Self::from_stmt(context, query).await
|
||||
}
|
||||
|
||||
pub async fn from_fingerprint(
|
||||
@@ -152,72 +156,77 @@ impl Peerstate {
|
||||
_sql: &Sql,
|
||||
fingerprint: &Fingerprint,
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
let fp = fingerprint.hex();
|
||||
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, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE public_key_fingerprint=? COLLATE NOCASE \
|
||||
OR gossip_key_fingerprint=? COLLATE NOCASE \
|
||||
ORDER BY public_key_fingerprint=? DESC;";
|
||||
let fp = fingerprint.hex();
|
||||
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
|
||||
ORDER BY public_key_fingerprint=? DESC;",
|
||||
)
|
||||
.bind(&fp)
|
||||
.bind(&fp)
|
||||
.bind(&fp);
|
||||
|
||||
Self::from_stmt(context, query).await
|
||||
}
|
||||
|
||||
async fn from_stmt(
|
||||
async fn from_stmt<'q, E>(
|
||||
context: &Context,
|
||||
query: &str,
|
||||
params: Vec<&dyn crate::ToSql>,
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let peerstate = context
|
||||
.sql
|
||||
.query_row_optional(query, params, |row| {
|
||||
/* all the above queries start with this: SELECT
|
||||
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
*/
|
||||
query: Query<'q, Sqlite, E>,
|
||||
) -> Result<Option<Peerstate>>
|
||||
where
|
||||
E: 'q + sqlx::IntoArguments<'q, sqlx::Sqlite>,
|
||||
{
|
||||
if let Some(row) = context.sql.fetch_optional(query).await? {
|
||||
// all the above queries start with this: SELECT
|
||||
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
|
||||
let res = Peerstate {
|
||||
addr: row.get(0)?,
|
||||
last_seen: row.get(1)?,
|
||||
last_seen_autocrypt: row.get(2)?,
|
||||
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
|
||||
public_key: row
|
||||
.get(4)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
public_key_fingerprint: row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_key: row
|
||||
.get(6)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
gossip_key_fingerprint: row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_timestamp: row.get(5)?,
|
||||
verified_key: row
|
||||
.get(9)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
verified_key_fingerprint: row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
let peerstate = Peerstate {
|
||||
addr: row.try_get(0)?,
|
||||
last_seen: row.try_get(1)?,
|
||||
last_seen_autocrypt: row.try_get(2)?,
|
||||
prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(),
|
||||
public_key: row
|
||||
.try_get::<&[u8], _>(4)
|
||||
.ok()
|
||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
||||
public_key_fingerprint: row
|
||||
.try_get::<Option<String>, _>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_key: row
|
||||
.try_get::<&[u8], _>(6)
|
||||
.ok()
|
||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
||||
gossip_key_fingerprint: row
|
||||
.try_get::<Option<String>, _>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_timestamp: row.try_get(5)?,
|
||||
verified_key: row
|
||||
.try_get::<&[u8], _>(9)
|
||||
.ok()
|
||||
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
|
||||
verified_key_fingerprint: row
|
||||
.try_get::<Option<String>, _>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
Ok(peerstate)
|
||||
Ok(Some(peerstate))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recalc_fingerprint(&mut self) {
|
||||
@@ -266,9 +275,8 @@ impl Peerstate {
|
||||
if self.fingerprint_changed {
|
||||
if let Some(contact_id) = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
paramsv![self.addr],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -429,42 +437,59 @@ impl Peerstate {
|
||||
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
|
||||
if self.to_save == Some(ToSave::All) || create {
|
||||
sql.execute(
|
||||
if create {
|
||||
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint, addr \
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
||||
(if create {
|
||||
sqlx::query(
|
||||
"INSERT INTO acpeerstates ( \
|
||||
last_seen, \
|
||||
last_seen_autocrypt, \
|
||||
prefer_encrypted, \
|
||||
public_key, \
|
||||
gossip_timestamp, \
|
||||
gossip_key, \
|
||||
public_key_fingerprint, \
|
||||
gossip_key_fingerprint, \
|
||||
verified_key, \
|
||||
verified_key_fingerprint, \
|
||||
addr \
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
||||
)
|
||||
} else {
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
|
||||
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
|
||||
verified_key=?, verified_key_fingerprint=? \
|
||||
WHERE addr=?"
|
||||
},
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
self.public_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.gossip_timestamp,
|
||||
self.gossip_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.addr,
|
||||
],
|
||||
).await?;
|
||||
sqlx::query(
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, \
|
||||
last_seen_autocrypt=?, \
|
||||
prefer_encrypted=?, \
|
||||
public_key=?, \
|
||||
gossip_timestamp=?, \
|
||||
gossip_key=?, \
|
||||
public_key_fingerprint=?, \
|
||||
gossip_key_fingerprint=?, \
|
||||
verified_key=?, \
|
||||
verified_key_fingerprint=? \
|
||||
WHERE addr=?",
|
||||
)
|
||||
})
|
||||
.bind(self.last_seen)
|
||||
.bind(self.last_seen_autocrypt)
|
||||
.bind(self.prefer_encrypt as i64)
|
||||
.bind(self.public_key.as_ref().map(|k| k.to_bytes()))
|
||||
.bind(self.gossip_timestamp)
|
||||
.bind(self.gossip_key.as_ref().map(|k| k.to_bytes()))
|
||||
.bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
||||
.bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
||||
.bind(self.verified_key.as_ref().map(|k| k.to_bytes()))
|
||||
.bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()))
|
||||
.bind(&self.addr),
|
||||
)
|
||||
.await?;
|
||||
} else if self.to_save == Some(ToSave::Timestamps) {
|
||||
sql.execute(
|
||||
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
||||
WHERE addr=?;",
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.gossip_timestamp,
|
||||
self.addr
|
||||
],
|
||||
sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
||||
WHERE addr=?;").bind(
|
||||
self.last_seen).bind(
|
||||
self.last_seen_autocrypt).bind(
|
||||
self.gossip_timestamp).bind(
|
||||
&self.addr)
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -481,12 +506,6 @@ 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -619,7 +638,7 @@ mod tests {
|
||||
// can be loaded without errors.
|
||||
ctx.ctx
|
||||
.sql
|
||||
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
|
||||
.execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr))
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,24 +10,24 @@ use chrono::{NaiveDateTime, NaiveTime};
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Status {
|
||||
OK = 1,
|
||||
PREPARATION = 2,
|
||||
BROKEN = 3,
|
||||
Ok = 1,
|
||||
Preparation = 2,
|
||||
Broken = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Protocol {
|
||||
SMTP = 1,
|
||||
IMAP = 2,
|
||||
Smtp = 1,
|
||||
Imap = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Socket {
|
||||
Automatic = 0,
|
||||
SSL = 1,
|
||||
STARTTLS = 2,
|
||||
Ssl = 1,
|
||||
Starttls = 2,
|
||||
Plain = 3,
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ impl Default for Socket {
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum UsernamePattern {
|
||||
EMAIL = 1,
|
||||
EMAILLOCALPART = 2,
|
||||
Email = 1,
|
||||
Emaillocalpart = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -151,6 +151,8 @@ pub async fn get_provider_by_mx(domain: impl AsRef<str>) -> Option<&'static Prov
|
||||
None
|
||||
}
|
||||
|
||||
// TODO: uncomment when clippy starts complaining about it
|
||||
//#[allow(clippy::manual_map)] // Can't use .map() because the lifetime is not propagated
|
||||
pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
|
||||
if let Some(provider) = PROVIDER_IDS.get(id) {
|
||||
Some(provider)
|
||||
@@ -181,34 +183,34 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_provider_by_domain_mixed_case() {
|
||||
let provider = get_provider_by_domain("nAUta.Cu").unwrap();
|
||||
assert!(provider.status == Status::OK);
|
||||
assert!(provider.status == Status::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_by_domain() {
|
||||
let addr = "nauta.cu";
|
||||
let provider = get_provider_by_domain(addr).unwrap();
|
||||
assert!(provider.status == Status::OK);
|
||||
assert!(provider.status == Status::Ok);
|
||||
let server = &provider.server[0];
|
||||
assert_eq!(server.protocol, Protocol::IMAP);
|
||||
assert_eq!(server.socket, Socket::STARTTLS);
|
||||
assert_eq!(server.protocol, Protocol::Imap);
|
||||
assert_eq!(server.socket, Socket::Starttls);
|
||||
assert_eq!(server.hostname, "imap.nauta.cu");
|
||||
assert_eq!(server.port, 143);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::Email);
|
||||
let server = &provider.server[1];
|
||||
assert_eq!(server.protocol, Protocol::SMTP);
|
||||
assert_eq!(server.socket, Socket::STARTTLS);
|
||||
assert_eq!(server.protocol, Protocol::Smtp);
|
||||
assert_eq!(server.socket, Socket::Starttls);
|
||||
assert_eq!(server.hostname, "smtp.nauta.cu");
|
||||
assert_eq!(server.port, 25);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::Email);
|
||||
|
||||
let provider = get_provider_by_domain("gmail.com").unwrap();
|
||||
assert!(provider.status == Status::PREPARATION);
|
||||
assert!(provider.status == Status::Preparation);
|
||||
assert!(!provider.before_login_hint.is_empty());
|
||||
assert!(!provider.overview_page.is_empty());
|
||||
|
||||
let provider = get_provider_by_domain("googlemail.com").unwrap();
|
||||
assert!(provider.status == Status::PREPARATION);
|
||||
assert!(provider.status == Status::Preparation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -103,8 +103,8 @@ def process_data(data, file):
|
||||
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
|
||||
raise TypeError("bad username pattern")
|
||||
|
||||
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
|
||||
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
|
||||
server += (" Server { protocol: " + protocol.capitalize() + ", socket: " + socket.capitalize() + ", hostname: \""
|
||||
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern.capitalize() + " },\n")
|
||||
|
||||
config_defaults = process_config_defaults(data)
|
||||
|
||||
@@ -123,7 +123,7 @@ def process_data(data, file):
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
|
||||
provider += " id: \"" + file2id(file) + "\",\n"
|
||||
provider += " status: Status::" + status + ",\n"
|
||||
provider += " status: Status::" + status.capitalize() + ",\n"
|
||||
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
|
||||
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
|
||||
provider += " overview_page: \"" + file2url(file) + "\",\n"
|
||||
@@ -175,7 +175,7 @@ if __name__ == "__main__":
|
||||
"use crate::provider::Protocol::*;\n"
|
||||
"use crate::provider::Socket::*;\n"
|
||||
"use crate::provider::UsernamePattern::*;\n"
|
||||
"use crate::provider::*;\n"
|
||||
"use crate::provider::{Config, ConfigDefault, Oauth2Authorizer, Provider, Server, Status};\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"use once_cell::sync::Lazy;\n\n")
|
||||
|
||||
|
||||
108
src/qr.rs
108
src/qr.rs
@@ -27,11 +27,11 @@ const HTTP_SCHEME: &str = "http://";
|
||||
const HTTPS_SCHEME: &str = "https://";
|
||||
|
||||
// Make it easy to convert errors into the final `Lot`.
|
||||
impl Into<Lot> for Error {
|
||||
fn into(self) -> Lot {
|
||||
let mut l = Lot::new();
|
||||
impl From<Error> for Lot {
|
||||
fn from(error: Error) -> Self {
|
||||
let mut l = Self::new();
|
||||
l.state = LotState::QrError;
|
||||
l.text1 = Some(self.to_string());
|
||||
l.text1 = Some(error.to_string());
|
||||
|
||||
l
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
|
||||
@@ -169,6 +170,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
.unwrap_or_default();
|
||||
|
||||
chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await;
|
||||
} else if let Some(addr) = addr {
|
||||
lot.state = LotState::QrFprMismatch;
|
||||
lot.id = match Contact::lookup_id_by_addr(context, &addr, Origin::Unknown).await {
|
||||
Ok(contact_id) => contact_id.unwrap_or_default(),
|
||||
Err(err) => {
|
||||
return format_err!("Error looking up contact {:?}: {}", addr, err).into()
|
||||
}
|
||||
};
|
||||
} else {
|
||||
lot.state = LotState::QrFprWithoutAddr;
|
||||
lot.text1 = Some(fingerprint.to_string());
|
||||
@@ -436,7 +445,10 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::key::DcKey;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_http() {
|
||||
@@ -625,6 +637,59 @@ mod tests {
|
||||
assert_eq!(contact.get_name(), "");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_openpgp_fingerprint() {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let contact_id = Contact::create(&ctx, "Alice", "alice@example.com")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.com".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
|
||||
"failed to save peerstate"
|
||||
);
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.com",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrFprMismatch);
|
||||
assert_eq!(res.get_id(), contact_id);
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrFprOk);
|
||||
assert_eq!(res.get_id(), contact_id);
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrFprMismatch);
|
||||
assert_eq!(res.get_id(), 0);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_openpgp_without_addr() {
|
||||
let ctx = TestContext::new().await;
|
||||
@@ -726,20 +791,39 @@ mod tests {
|
||||
async fn test_set_config_from_qr() {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
assert!(ctx
|
||||
.ctx
|
||||
.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||
assert!(!res.is_ok());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
assert!(ctx
|
||||
.ctx
|
||||
.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
|
||||
assert!(!res.is_ok());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
assert!(ctx
|
||||
.ctx
|
||||
.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
|
||||
ctx.ctx
|
||||
.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"https://example.org/"
|
||||
);
|
||||
|
||||
@@ -747,7 +831,11 @@ mod tests {
|
||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
|
||||
ctx.ctx
|
||||
.get_config(Config::WebrtcInstance)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
Some(job) => {
|
||||
// Let the fetch run, but return back to the job afterwards.
|
||||
jobs_loaded = 0;
|
||||
if ctx.get_config_bool(Config::InboxWatch).await {
|
||||
if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "postponing imap-job {} to run fetch...", job);
|
||||
fetch(&ctx, &mut connection).await;
|
||||
}
|
||||
@@ -93,7 +97,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
|
||||
maybe_add_time_based_warnings(&ctx).await;
|
||||
|
||||
info = if ctx.get_config_bool(Config::InboxWatch).await {
|
||||
info = if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
if let Err(err) = connection.scan_folders(&ctx).await {
|
||||
@@ -121,7 +129,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
|
||||
async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
match ctx.get_config(Config::ConfiguredInboxFolder).await {
|
||||
Some(watch_folder) => {
|
||||
Ok(Some(watch_folder)) => {
|
||||
if let Err(err) = connection.connect_configured(ctx).await {
|
||||
error_network!(ctx, "{}", err);
|
||||
return;
|
||||
@@ -133,16 +141,23 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
Ok(None) => {
|
||||
warn!(ctx, "Can not fetch inbox folder, not set");
|
||||
connection.fake_idle(ctx, None).await;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
ctx,
|
||||
"Can not fetch inbox folder, failed to get config: {:?}", err
|
||||
);
|
||||
connection.fake_idle(ctx, None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
|
||||
match ctx.get_config(folder).await {
|
||||
Some(watch_folder) => {
|
||||
Ok(Some(watch_folder)) => {
|
||||
// connect and fake idle if unable to connect
|
||||
if let Err(err) = connection.connect_configured(ctx).await {
|
||||
warn!(ctx, "imap connection failed: {}", err);
|
||||
@@ -178,10 +193,17 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
connection.fake_idle(ctx, Some(watch_folder)).await
|
||||
}
|
||||
}
|
||||
None => {
|
||||
Ok(None) => {
|
||||
warn!(ctx, "Can not watch {} folder, not set", folder);
|
||||
connection.fake_idle(ctx, None).await
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
ctx,
|
||||
"Can not watch {} folder, failed to retrieve config: {:?}", folder, err
|
||||
);
|
||||
connection.fake_idle(ctx, None).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +321,11 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
if ctx.get_config_bool(Config::MvboxWatch).await {
|
||||
if ctx
|
||||
.get_config_bool(Config::MvboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -317,7 +343,11 @@ impl Scheduler {
|
||||
.expect("mvbox start send, missing receiver");
|
||||
}
|
||||
|
||||
if ctx.get_config_bool(Config::SentboxWatch).await {
|
||||
if ctx
|
||||
.get_config_bool(Config::SentboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
sentbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -523,9 +553,9 @@ impl SmtpConnectionState {
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
idle_interrupt_sender,
|
||||
shutdown_receiver,
|
||||
stop_sender,
|
||||
idle_interrupt_sender,
|
||||
};
|
||||
|
||||
let conn = SmtpConnectionState { state };
|
||||
@@ -570,9 +600,9 @@ impl ImapConnectionState {
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
idle_interrupt_sender,
|
||||
shutdown_receiver,
|
||||
stop_sender,
|
||||
idle_interrupt_sender,
|
||||
};
|
||||
|
||||
let conn = ImapConnectionState { state };
|
||||
|
||||
@@ -60,14 +60,11 @@ pub struct BobStateHandle<'a> {
|
||||
impl<'a> BobStateHandle<'a> {
|
||||
/// Creates a new instance, upholding the guarantee that [`BobState`] must exist.
|
||||
pub fn from_guard(mut guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
|
||||
match guard.take() {
|
||||
Some(bobstate) => Some(Self {
|
||||
guard,
|
||||
bobstate,
|
||||
clear_state_on_drop: false,
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
guard.take().map(|bobstate| Self {
|
||||
guard,
|
||||
bobstate,
|
||||
clear_state_on_drop: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
|
||||
@@ -191,7 +188,7 @@ impl BobState {
|
||||
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await {
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
let state = Self {
|
||||
@@ -300,7 +297,7 @@ impl BobState {
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(reason)));
|
||||
}
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await {
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
|
||||
}
|
||||
|
||||
@@ -173,9 +173,16 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
|
||||
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
|
||||
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
|
||||
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
error!(context, "Not configured, cannot generate QR code.",);
|
||||
Ok(Some(addr)) => addr,
|
||||
Ok(None) => {
|
||||
error!(context, "Not configured, cannot generate QR code.");
|
||||
return None;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Unable to retrieve configuration, cannot generate QR code: {:?}", err
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -183,6 +190,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.ok()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
|
||||
@@ -263,6 +271,8 @@ pub enum JoinError {
|
||||
MissingChat(#[source] sql::Error),
|
||||
#[error("Ongoing sender dropped (this is a bug)")]
|
||||
OngoingSenderDropped,
|
||||
#[error("Other")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
@@ -290,6 +300,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
let qr_scan = check_qr(context, &qr).await;
|
||||
|
||||
let invite = QrInvite::try_from(qr_scan)?;
|
||||
|
||||
match context.bob.start_protocol(context, invite.clone()).await? {
|
||||
@@ -390,11 +401,11 @@ async fn send_handshake_msg(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
contact_id
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
|
||||
Ok(contact_id)
|
||||
} else {
|
||||
0
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,8 +413,8 @@ async fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
contact_chat_id: ChatId,
|
||||
) -> bool {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
) -> Result<bool, Error> {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
@@ -414,7 +425,7 @@ async fn fingerprint_equals_sender(
|
||||
contact.get_addr(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -422,12 +433,12 @@ async fn fingerprint_equals_sender(
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
@@ -552,7 +563,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(_stage) => {
|
||||
@@ -581,7 +592,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
@@ -591,16 +602,16 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not encrypted.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
@@ -613,13 +624,13 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not provided.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::Auth, auth_0).await {
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
|
||||
@@ -628,12 +639,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
|
||||
info!(context, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
@@ -693,12 +704,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(BobHandshakeStage::Completed) => {
|
||||
// Can only be BobHandshakeStage::Completed
|
||||
secure_connection_established(context, bobstate.chat_id()).await;
|
||||
secure_connection_established(context, bobstate.chat_id()).await?;
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
@@ -812,7 +823,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
"Message not encrypted correctly.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
|
||||
@@ -824,7 +835,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
@@ -834,7 +845,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Ok(if step.as_str() == "vg-member-added" {
|
||||
@@ -847,8 +858,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
}
|
||||
}
|
||||
|
||||
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
|
||||
async fn secure_connection_established(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
) -> Result<(), Error> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
|
||||
let addr = if let Ok(ref contact) = contact {
|
||||
@@ -860,14 +874,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, EventType::ChatModified(contact_chat_id));
|
||||
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
details: &str,
|
||||
) {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
|
||||
) -> Result<(), Error> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
let msg = stock_str::contact_not_verified(
|
||||
context,
|
||||
@@ -884,6 +900,8 @@ async fn could_not_establish_secure_connection(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
|
||||
@@ -1061,6 +1079,7 @@ mod tests {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
@@ -1109,6 +1128,7 @@ mod tests {
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
|
||||
@@ -22,25 +22,24 @@ const SMTP_TIMEOUT: u64 = 30;
|
||||
pub enum Error {
|
||||
#[error("Bad parameters")]
|
||||
BadParameters,
|
||||
|
||||
#[error("Invalid login address {address}: {error}")]
|
||||
InvalidLoginAddress {
|
||||
address: String,
|
||||
#[source]
|
||||
error: error::Error,
|
||||
},
|
||||
|
||||
#[error("SMTP: failed to connect: {0}")]
|
||||
ConnectionFailure(#[source] smtp::error::Error),
|
||||
|
||||
#[error("SMTP: failed to setup connection {0:?}")]
|
||||
ConnectionSetupFailure(#[source] smtp::error::Error),
|
||||
|
||||
#[error("SMTP: oauth2 error {address}")]
|
||||
Oauth2Error { address: String },
|
||||
|
||||
#[error("TLS error")]
|
||||
#[error("TLS error {0}")]
|
||||
Tls(#[from] async_native_tls::Error),
|
||||
#[error("Sql {0}")]
|
||||
Sql(#[from] crate::sql::Error),
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -100,7 +99,7 @@ impl Smtp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lp = LoginParam::from_database(context, "configured_").await;
|
||||
let lp = LoginParam::from_database(context, "configured_").await?;
|
||||
let res = self
|
||||
.connect(
|
||||
context,
|
||||
@@ -164,7 +163,7 @@ impl Smtp {
|
||||
let (creds, mechanism) = if oauth2 {
|
||||
// oauth2
|
||||
let send_pw = &lp.password;
|
||||
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await;
|
||||
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
|
||||
if access_token.is_none() {
|
||||
return Err(Error::Oauth2Error {
|
||||
address: addr.to_string(),
|
||||
@@ -193,7 +192,7 @@ impl Smtp {
|
||||
|
||||
let security = match lp.security {
|
||||
Socket::Plain => smtp::ClientSecurity::None,
|
||||
Socket::STARTTLS => smtp::ClientSecurity::Required(tls_parameters),
|
||||
Socket::Starttls => smtp::ClientSecurity::Required(tls_parameters),
|
||||
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub enum Error {
|
||||
#[error("Envelope error: {}", _0)]
|
||||
EnvelopeError(#[from] async_smtp::error::Error),
|
||||
|
||||
#[error("Send error: {}", _0)]
|
||||
SendError(#[from] async_smtp::smtp::error::Error),
|
||||
|
||||
#[error("SMTP has no transport")]
|
||||
NoTransport,
|
||||
#[error("{}", _0)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
@@ -36,7 +36,7 @@ impl Smtp {
|
||||
let message_len_bytes = message.len();
|
||||
|
||||
let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO;
|
||||
if let Some(provider) = context.get_configured_provider().await {
|
||||
if let Some(provider) = context.get_configured_provider().await? {
|
||||
if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to {
|
||||
chunk_size = max_smtp_rcpt_to as usize;
|
||||
}
|
||||
|
||||
1665
src/sql.rs
1665
src/sql.rs
File diff suppressed because it is too large
Load Diff
19
src/sql/error.rs
Normal file
19
src/sql/error.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
#[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>;
|
||||
505
src/sql/migrations.rs
Normal file
505
src/sql/migrations.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
use async_std::prelude::*;
|
||||
|
||||
use super::{Result, Sql};
|
||||
use crate::config::Config;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::imap;
|
||||
use crate::provider::get_provider_by_domain;
|
||||
|
||||
const DBVERSION: i32 = 68;
|
||||
const VERSION_CFG: &str = "dbversion";
|
||||
const TABLES: &str = include_str!("./tables.sql");
|
||||
|
||||
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
|
||||
let mut recalc_fingerprints = false;
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update = DBVERSION;
|
||||
|
||||
if !sql.table_exists("config").await? {
|
||||
info!(context, "First time init: creating tables",);
|
||||
sql.transaction(move |conn| {
|
||||
Box::pin(async move {
|
||||
sqlx::query(TABLES)
|
||||
.execute_many(&mut *conn)
|
||||
.await
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.await?;
|
||||
|
||||
// set raw config inside the transaction
|
||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
||||
.bind(VERSION_CFG)
|
||||
.bind(format!("{}", dbversion_before_update))
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
exists_before_update = true;
|
||||
dbversion_before_update = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let dbversion = dbversion_before_update;
|
||||
let mut update_icons = !exists_before_update;
|
||||
let mut disable_server_delete = false;
|
||||
|
||||
if dbversion < 1 {
|
||||
info!(context, "[migration] v1");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
|
||||
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 2 {
|
||||
info!(context, "[migration] v2");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
|
||||
2,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 7 {
|
||||
info!(context, "[migration] v7");
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE keypairs (\
|
||||
id INTEGER PRIMARY KEY, \
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE, \
|
||||
is_default INTEGER DEFAULT 0, \
|
||||
private_key, \
|
||||
public_key, \
|
||||
created INTEGER DEFAULT 0);",
|
||||
7,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 10 {
|
||||
info!(context, "[migration] v10");
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE acpeerstates (\
|
||||
id INTEGER PRIMARY KEY, \
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE, \
|
||||
last_seen INTEGER DEFAULT 0, \
|
||||
last_seen_autocrypt INTEGER DEFAULT 0, \
|
||||
public_key, \
|
||||
prefer_encrypted INTEGER DEFAULT 0); \
|
||||
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);",
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 12 {
|
||||
info!(context, "[migration] v12");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);
|
||||
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
|
||||
12,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 17 {
|
||||
info!(context, "[migration] v17");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
-- 'starred' column is not used currently
|
||||
-- (dropping is not easily doable and stop adding it will make reusing it complicated)
|
||||
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);"#,
|
||||
17,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 18 {
|
||||
info!(context, "[migration] v18");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
|
||||
18,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 27 {
|
||||
info!(context, "[migration] v27");
|
||||
// chat.id=1 and chat.id=2 are the old deaddrops,
|
||||
// the current ones are defined by chats.blocked=2
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;"
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);"
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;")
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
|
||||
27,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 34 {
|
||||
info!(context, "[migration] v34");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
|
||||
ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
|
||||
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
|
||||
34,
|
||||
)
|
||||
.await?;
|
||||
recalc_fingerprints = true;
|
||||
}
|
||||
if dbversion < 39 {
|
||||
info!(context, "[migration] v39");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
namespc INTEGER DEFAULT 0,
|
||||
foreign_id INTEGER DEFAULT 0,
|
||||
token TEXT DEFAULT '',
|
||||
timestamp INTEGER DEFAULT 0
|
||||
);
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key;
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
|
||||
38,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 40 {
|
||||
info!(context, "[migration] v40");
|
||||
sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 44 {
|
||||
info!(context, "[migration] v44");
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 46 {
|
||||
info!(context, "[migration] v46");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
|
||||
ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
|
||||
46,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 47 {
|
||||
info!(context, "[migration] v47");
|
||||
sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 48 {
|
||||
info!(context, "[migration] v48");
|
||||
// NOTE: move_state is not used anymore
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
|
||||
48,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 49 {
|
||||
info!(context, "[migration] v49");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
|
||||
49,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 50 {
|
||||
info!(context, "[migration] v50");
|
||||
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
|
||||
// keep this default and use DC_SHOW_EMAILS_NO
|
||||
// only for new installations
|
||||
if exists_before_update {
|
||||
sql.set_raw_config_int("show_emails", ShowEmails::All as i32)
|
||||
.await?;
|
||||
}
|
||||
sql.set_db_version(50).await?;
|
||||
}
|
||||
if dbversion < 53 {
|
||||
info!(context, "[migration] v53");
|
||||
// the messages containing _only_ locations
|
||||
// are also added to the database as _hidden_.
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL DEFAULT 0.0,
|
||||
longitude REAL DEFAULT 0.0,
|
||||
accuracy REAL DEFAULT 0.0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0
|
||||
);"
|
||||
CREATE INDEX locations_index1 ON locations (from_id);
|
||||
CREATE INDEX locations_index2 ON locations (timestamp);
|
||||
ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
|
||||
ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
|
||||
ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
|
||||
53,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 54 {
|
||||
info!(context, "[migration] v54");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index6 ON msgs (location_id);"#,
|
||||
54,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 55 {
|
||||
info!(context, "[migration] v55");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
|
||||
55,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 59 {
|
||||
info!(context, "[migration] v59");
|
||||
// records in the devmsglabels are kept when the message is deleted.
|
||||
// so, msg_id may or may not exist.
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);",
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
|
||||
.await?;
|
||||
if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() {
|
||||
sql.set_raw_config_int("bcc_self", 1).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if dbversion < 60 {
|
||||
info!(context, "[migration] v60");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
|
||||
60,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 61 {
|
||||
info!(context, "[migration] v61");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
|
||||
61,
|
||||
)
|
||||
.await?;
|
||||
update_icons = true;
|
||||
}
|
||||
if dbversion < 62 {
|
||||
info!(context, "[migration] v62");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
|
||||
62,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 63 {
|
||||
info!(context, "[migration] v63");
|
||||
sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 64 {
|
||||
info!(context, "[migration] v64");
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 65 {
|
||||
info!(context, "[migration] v65");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
|
||||
ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
|
||||
65,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 66 {
|
||||
info!(context, "[migration] v66");
|
||||
update_icons = true;
|
||||
sql.set_db_version(66).await?;
|
||||
}
|
||||
if dbversion < 67 {
|
||||
info!(context, "[migration] v67");
|
||||
for prefix in &["", "configured_"] {
|
||||
if let Some(server_flags) = sql
|
||||
.get_raw_config_int(format!("{}server_flags", prefix))
|
||||
.await?
|
||||
{
|
||||
let imap_socket_flags = server_flags & 0x700;
|
||||
let key = format!("{}mail_security", prefix);
|
||||
match imap_socket_flags {
|
||||
0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
|
||||
0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
|
||||
0x400 => sql.set_raw_config_int(key, 3).await?, // Plain
|
||||
_ => sql.set_raw_config_int(key, 0).await?,
|
||||
}
|
||||
let smtp_socket_flags = server_flags & 0x70000;
|
||||
let key = format!("{}send_security", prefix);
|
||||
match smtp_socket_flags {
|
||||
0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
|
||||
0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
|
||||
0x40000 => sql.set_raw_config_int(key, 3).await?, // Plain
|
||||
_ => sql.set_raw_config_int(key, 0).await?,
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.set_db_version(67).await?;
|
||||
}
|
||||
if dbversion < 68 {
|
||||
info!(context, "[migration] v68");
|
||||
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
|
||||
sql.execute_migration(
|
||||
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
|
||||
68,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 69 {
|
||||
info!(context, "[migration] v69");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
|
||||
-- 120=group, 130=old verified group
|
||||
UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
|
||||
69,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 71 {
|
||||
info!(context, "[migration] v71");
|
||||
if let Some(addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
|
||||
context
|
||||
.set_config(
|
||||
Config::ConfiguredProvider,
|
||||
get_provider_by_domain(&domain).map(|provider| provider.id),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Can't parse configured address: {:?}", addr);
|
||||
}
|
||||
}
|
||||
|
||||
sql.set_db_version(71).await?;
|
||||
}
|
||||
if dbversion < 72 {
|
||||
info!(context, "[migration] v72");
|
||||
if !sql.col_exists("msgs", "mime_modified").await? {
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
|
||||
72,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if dbversion < 73 {
|
||||
use Config::*;
|
||||
info!(context, "[migration] v73");
|
||||
sql.execute(sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#),
|
||||
)
|
||||
.await?;
|
||||
for c in &[
|
||||
ConfiguredInboxFolder,
|
||||
ConfiguredSentboxFolder,
|
||||
ConfiguredMvboxFolder,
|
||||
] {
|
||||
if let Some(folder) = context.get_config(*c).await? {
|
||||
let (uid_validity, last_seen_uid) =
|
||||
imap::get_config_last_seen_uid(context, &folder).await?;
|
||||
if last_seen_uid > 0 {
|
||||
imap::set_uid_next(context, &folder, last_seen_uid + 1).await?;
|
||||
imap::set_uidvalidity(context, &folder, uid_validity).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if exists_before_update {
|
||||
disable_server_delete = true;
|
||||
|
||||
// Don't disable server delete if it was on by default (Nauta):
|
||||
if let Some(provider) = context.get_configured_provider().await? {
|
||||
if let Some(defaults) = &provider.config_defaults {
|
||||
if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) {
|
||||
disable_server_delete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.set_db_version(73).await?;
|
||||
}
|
||||
if dbversion < 74 {
|
||||
info!(context, "[migration] v74");
|
||||
sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 75 {
|
||||
info!(context, "[migration] v75");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
|
||||
74,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 76 {
|
||||
info!(context, "[migration] v76");
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((recalc_fingerprints, update_icons, disable_server_delete))
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
async fn set_db_version(&self, version: i32) -> Result<()> {
|
||||
self.set_raw_config_int(VERSION_CFG, version).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> {
|
||||
let query = sqlx::query(query);
|
||||
self.transaction(move |conn| {
|
||||
Box::pin(async move {
|
||||
query
|
||||
.execute_many(&mut *conn)
|
||||
.await
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.await?;
|
||||
|
||||
// set raw config inside the transaction
|
||||
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
|
||||
.bind(format!("{}", version))
|
||||
.bind(VERSION_CFG)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
868
src/sql/mod.rs
Normal file
868
src/sql/mod.rs
Normal file
@@ -0,0 +1,868 @@
|
||||
//! # SQLite wrapper
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::RwLock;
|
||||
use sqlx::{
|
||||
pool::PoolOptions,
|
||||
query::Query,
|
||||
sqlite::{Sqlite, SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
|
||||
Executor, IntoArguments, Row,
|
||||
};
|
||||
|
||||
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, time};
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::message::Message;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::stock_str;
|
||||
|
||||
mod error;
|
||||
mod migrations;
|
||||
|
||||
pub use self::error::*;
|
||||
|
||||
/// 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)]
|
||||
pub struct Sql {
|
||||
/// Writer pool, must only have 1 connection in it.
|
||||
writer: RwLock<Option<SqlitePool>>,
|
||||
/// Reader pool, maintains multiple connections for reading data.
|
||||
reader: RwLock<Option<SqlitePool>>,
|
||||
}
|
||||
|
||||
impl Default for Sql {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
writer: RwLock::new(None),
|
||||
reader: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Sql {
|
||||
fn drop(&mut self) {
|
||||
async_std::task::block_on(self.close());
|
||||
}
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
pub fn new() -> Sql {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Checks if there is currently a connection to the underlying Sqlite database.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
// in read only mode the writer does not exists
|
||||
self.reader.read().await.is_some()
|
||||
}
|
||||
|
||||
/// Closes all underlying Sqlite connections.
|
||||
pub async fn close(&self) {
|
||||
if let Some(sql) = self.writer.write().await.take() {
|
||||
sql.close().await;
|
||||
}
|
||||
if let Some(sql) = self.reader.write().await.take() {
|
||||
sql.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_writer_pool(dbfile: impl AsRef<Path>) -> sqlx::Result<SqlitePool> {
|
||||
let config = SqliteConnectOptions::new()
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.filename(dbfile.as_ref())
|
||||
.read_only(false)
|
||||
.busy_timeout(Duration::from_secs(100))
|
||||
.create_if_missing(true)
|
||||
.synchronous(SqliteSynchronous::Normal);
|
||||
|
||||
PoolOptions::<Sqlite>::new()
|
||||
.max_connections(1)
|
||||
.after_connect(|conn| {
|
||||
Box::pin(async move {
|
||||
let q = r#"
|
||||
PRAGMA secure_delete=on;
|
||||
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
||||
"#;
|
||||
|
||||
conn.execute_many(sqlx::query(q))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.connect_with(config)
|
||||
.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)
|
||||
.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
|
||||
"#;
|
||||
|
||||
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.
|
||||
/// If a database is already open, this will return an error.
|
||||
pub async fn open(
|
||||
&self,
|
||||
context: &Context,
|
||||
dbfile: impl AsRef<Path>,
|
||||
readonly: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
"Cannot open, database \"{:?}\" already opened.",
|
||||
dbfile.as_ref(),
|
||||
);
|
||||
return Err(Error::SqlAlreadyOpen.into());
|
||||
}
|
||||
|
||||
// Open write pool
|
||||
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 {
|
||||
// (1) update low-level database structure.
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete) =
|
||||
migrations::run(context, self).await?;
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// the structure is complete now and all objects are usable
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let mut rows = self
|
||||
.fetch(sqlx::query("SELECT addr FROM acpeerstates;"))
|
||||
.await?;
|
||||
|
||||
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? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(self, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
|
||||
if disable_server_delete {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config(Config::DeleteServerAfter, Some("0"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Opened {:?}.", dbfile.as_ref());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the given query, returning the number of affected rows.
|
||||
pub async fn execute<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<u64>
|
||||
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.rows_affected())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
query: Query<'q, Sqlite, E>,
|
||||
) -> Result<impl Stream<Item = sqlx::Result<<Sqlite as sqlx::Database>::Row>> + Send + 'q>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
{
|
||||
let lock = self.reader.read().await;
|
||||
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.
|
||||
pub async fn fetch_one<'q, E>(
|
||||
&self,
|
||||
query: Query<'q, Sqlite, E>,
|
||||
) -> Result<<Sqlite as sqlx::Database>::Row>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
{
|
||||
let lock = self.reader.read().await;
|
||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
||||
|
||||
let row = pool.fetch_one(query).await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Fetches at most one row.
|
||||
pub async fn fetch_optional<'e, 'q, E>(
|
||||
&self,
|
||||
query: Query<'q, Sqlite, E>,
|
||||
) -> Result<Option<<Sqlite as sqlx::Database>::Row>>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
{
|
||||
let lock = self.reader.read().await;
|
||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
||||
|
||||
let row = pool.fetch_optional(query).await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// 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>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
{
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let row = self.fetch_one(query).await?;
|
||||
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
|
||||
/// one, `false` otherwise.
|
||||
pub async fn exists<'e, 'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<bool>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
{
|
||||
let count = self.count(query).await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Execute the function inside a transaction.
|
||||
///
|
||||
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
||||
/// error, the transaction will be committed.
|
||||
pub async fn transaction<F, R>(&self, callback: F) -> Result<R>
|
||||
where
|
||||
F: for<'c> FnOnce(
|
||||
&'c mut sqlx::Transaction<'_, Sqlite>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + 'c + Send>>
|
||||
+ 'static
|
||||
+ Send
|
||||
+ Sync,
|
||||
R: Send,
|
||||
{
|
||||
let lock = self.writer.read().await;
|
||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let ret = callback(&mut transaction).await;
|
||||
|
||||
match ret {
|
||||
Ok(ret) => {
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
Err(err) => {
|
||||
transaction.rollback().await?;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the database if the requested table already exists.
|
||||
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
|
||||
let q = format!("PRAGMA table_info(\"{}\")", name.as_ref());
|
||||
|
||||
let lock = self.reader.read().await;
|
||||
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.
|
||||
pub async fn col_exists(
|
||||
&self,
|
||||
table_name: impl AsRef<str>,
|
||||
col_name: impl AsRef<str>,
|
||||
) -> Result<bool> {
|
||||
let q = format!("PRAGMA table_info(\"{}\")", table_name.as_ref());
|
||||
let lock = self.reader.read().await;
|
||||
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
|
||||
|
||||
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(false)
|
||||
}
|
||||
|
||||
/// Executes a query which is expected to return one row and one
|
||||
/// column. If the query does not return a value or returns SQL
|
||||
/// `NULL`, returns `Ok(None)`.
|
||||
pub async fn query_get_value<'e, 'q, E, T>(
|
||||
&self,
|
||||
query: Query<'q, Sqlite, E>,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
E: 'q + IntoArguments<'q, Sqlite>,
|
||||
T: for<'r> sqlx::Decode<'r, Sqlite> + sqlx::Type<Sqlite>,
|
||||
{
|
||||
let res = self
|
||||
.fetch_optional(query)
|
||||
.await?
|
||||
.map(|row| row.get::<T, _>(0));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Set private configuration options.
|
||||
///
|
||||
/// Setting `None` deletes the value. On failure an error message
|
||||
/// will already have been logged.
|
||||
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
|
||||
if !self.is_open().await {
|
||||
return Err(Error::SqlNoConnection);
|
||||
}
|
||||
|
||||
let key = key.as_ref();
|
||||
if let Some(value) = value {
|
||||
let exists = self
|
||||
.exists(sqlx::query("SELECT COUNT(*) FROM config WHERE keyname=?;").bind(key))
|
||||
.await?;
|
||||
|
||||
if exists {
|
||||
self.execute(
|
||||
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
|
||||
.bind(value)
|
||||
.bind(key),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
self.execute(
|
||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
||||
.bind(key)
|
||||
.bind(value),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
self.execute(sqlx::query("DELETE FROM config WHERE keyname=?;").bind(key))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get configuration options from the database.
|
||||
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
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT value FROM config WHERE keyname=?;").bind(key.as_ref()),
|
||||
)
|
||||
.await
|
||||
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> {
|
||||
self.set_raw_config(key, Some(&format!("{}", value))).await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Result<Option<i32>> {
|
||||
self.get_raw_config(key)
|
||||
.await
|
||||
.map(|s| s.and_then(|s| s.parse().ok()))
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> Result<bool> {
|
||||
// Not the most obvious way to encode bool as string, but it is matter
|
||||
// of backward compatibility.
|
||||
let res = self.get_raw_config_int(key).await?;
|
||||
Ok(res.unwrap_or_default() > 0)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_bool<T>(&self, key: T, value: bool) -> Result<()>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let value = if value { Some("1") } else { None };
|
||||
self.set_raw_config(key, value).await
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int64(&self, key: impl AsRef<str>, value: i64) -> Result<()> {
|
||||
self.set_raw_config(key, Some(&format!("{}", value))).await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Result<Option<i64>> {
|
||||
self.get_raw_config(key)
|
||||
.await
|
||||
.map(|s| s.and_then(|r| r.parse().ok()))
|
||||
}
|
||||
|
||||
/// Alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
|
||||
/// the ORDER BY ensures, this function always returns the most recent id,
|
||||
/// eg. if a Message-ID is split into different messages.
|
||||
pub async fn get_rowid(
|
||||
&self,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: impl AsRef<str>,
|
||||
) -> Result<i64> {
|
||||
// alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
|
||||
// the ORDER BY ensures, this function always returns the most recent id,
|
||||
// eg. if a Message-ID is split into different messages.
|
||||
let query = format!(
|
||||
"SELECT id FROM {} WHERE {}=? ORDER BY id DESC",
|
||||
table.as_ref(),
|
||||
field.as_ref(),
|
||||
);
|
||||
|
||||
self.query_get_value(sqlx::query(&query).bind(value.as_ref()))
|
||||
.await
|
||||
.map(|id| id.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Fetches the rowid by restricting the rows through two different key, value settings.
|
||||
pub async fn get_rowid2(
|
||||
&self,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: i64,
|
||||
field2: impl AsRef<str>,
|
||||
value2: i64,
|
||||
) -> Result<i64> {
|
||||
let query = format!(
|
||||
"SELECT id FROM {} WHERE {}={} AND {}={} ORDER BY id DESC",
|
||||
table.as_ref(),
|
||||
field.as_ref(),
|
||||
value,
|
||||
field2.as_ref(),
|
||||
value2,
|
||||
);
|
||||
|
||||
self.query_get_value(sqlx::query(&query))
|
||||
.await
|
||||
.map(|id| id.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
if let Err(err) = crate::ephemeral::delete_expired_messages(context).await {
|
||||
warn!(context, "Failed to delete expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut files_in_use = HashSet::new();
|
||||
let mut unreferenced_count = 0;
|
||||
|
||||
info!(context, "Start housekeeping...");
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;",
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM jobs;",
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM chats;",
|
||||
Param::ProfileImage,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM contacts;",
|
||||
Param::ProfileImage,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut rows = context
|
||||
.sql
|
||||
.fetch(sqlx::query("SELECT value FROM config;"))
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row: String = row?.try_get(0)?;
|
||||
maybe_add_file(&mut files_in_use, row);
|
||||
}
|
||||
|
||||
info!(context, "{} files in use.", files_in_use.len(),);
|
||||
/* go through directory and delete unused files */
|
||||
let p = context.get_blobdir();
|
||||
match async_std::fs::read_dir(p).await {
|
||||
Ok(mut dir_handle) => {
|
||||
/* avoid deletion of files that are just created to build a message object */
|
||||
let diff = std::time::Duration::from_secs(60 * 60);
|
||||
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
|
||||
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
if entry.is_err() {
|
||||
break;
|
||||
}
|
||||
let entry = entry.unwrap();
|
||||
let name_f = entry.file_name();
|
||||
let name_s = name_f.to_string_lossy();
|
||||
|
||||
if is_file_in_use(&files_in_use, None, &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
unreferenced_count += 1;
|
||||
|
||||
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
|
||||
let recently_created =
|
||||
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
|
||||
let recently_modified = stats.modified().is_ok()
|
||||
&& stats.modified().unwrap() > keep_files_newer_than;
|
||||
let recently_accessed = stats.accessed().is_ok()
|
||||
&& stats.accessed().unwrap() > keep_files_newer_than;
|
||||
|
||||
if recently_created || recently_modified || recently_accessed {
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Keeping new unreferenced file #{}: {:?}",
|
||||
unreferenced_count,
|
||||
entry.file_name(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Deleting unreferenced file #{}: {:?}",
|
||||
unreferenced_count,
|
||||
entry.file_name()
|
||||
);
|
||||
let path = entry.path();
|
||||
dc_delete_file(context, path).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot open {}. ({})",
|
||||
context.get_blobdir().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = start_ephemeral_timers(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: cannot start ephemeral timers: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(&context.sql).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = context
|
||||
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
||||
.await
|
||||
{
|
||||
warn!(context, "Can't set config: {}", e);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
|
||||
let name_to_check = if let Some(namespc) = namespc_opt {
|
||||
let name_len = name.len();
|
||||
let namespc_len = namespc.len();
|
||||
if name_len <= namespc_len || !name.ends_with(namespc) {
|
||||
return false;
|
||||
}
|
||||
&name[..name_len - namespc_len]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
files_in_use.contains(name_to_check)
|
||||
}
|
||||
|
||||
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
|
||||
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") {
|
||||
files_in_use.insert(file.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
async fn maybe_add_from_param(
|
||||
sql: &Sql,
|
||||
files_in_use: &mut HashSet<String>,
|
||||
query: &str,
|
||||
param_id: Param,
|
||||
) -> Result<()> {
|
||||
let mut rows = sql.fetch(sqlx::query(query)).await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row: String = row?.try_get(0)?;
|
||||
let param: Params = row.parse().unwrap_or_default();
|
||||
if let Some(file) = param.get(param_id) {
|
||||
maybe_add_file(files_in_use, file);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
sql.execute(
|
||||
sqlx::query(
|
||||
"DELETE FROM msgs \
|
||||
WHERE (chat_id = ? OR hidden) \
|
||||
AND server_uid = 0",
|
||||
)
|
||||
.bind(DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
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)]
|
||||
mod test {
|
||||
use async_std::fs::File;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::{test_utils::TestContext, Event, EventType};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_maybe_add_file() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
maybe_add_file(&mut files, "$BLOBDIR");
|
||||
|
||||
assert!(files.contains("hello"));
|
||||
assert!(files.contains("world.txt"));
|
||||
assert!(!files.contains("world2.txt"));
|
||||
assert!(!files.contains("$BLOBDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file_in_use() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
|
||||
assert!(is_file_in_use(&files, None, "hello"));
|
||||
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
||||
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_table_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
|
||||
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_col_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_housekeeping_db_closed() {
|
||||
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();
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
t.add_event_sink(move |event: Event| async move {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
"File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)",
|
||||
s
|
||||
),
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
|
||||
t.sql.open(&t, &t.get_dbfile(), false).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
///
|
||||
/// Previously the code checking for existence of `config` table
|
||||
/// checked it with `PRAGMA table_info("config")` but did not
|
||||
/// drain `SqlitePool.fetch` result, only using the first row
|
||||
/// returned. As a result, prepared statement for `PRAGMA` was not
|
||||
/// finalized early enough, leaving reader connection in a broken
|
||||
/// state after reopening the database, when `config` table
|
||||
/// existed and `PRAGMA` returned non-empty result.
|
||||
///
|
||||
/// Statements were not finalized due to a bug in sqlx:
|
||||
/// https://github.com/launchbadge/sqlx/issues/1147
|
||||
#[async_std::test]
|
||||
async fn test_db_reopen() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new();
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t, &dbfile, false).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t, &dbfile, false).await?;
|
||||
sql.execute(
|
||||
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
|
||||
.bind("foo")
|
||||
.bind("bar"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let value: Option<String> = sql
|
||||
.query_get_value(sqlx::query("SELECT value FROM config WHERE keyname=?;").bind("foo"))
|
||||
.await?;
|
||||
assert_eq!(value.unwrap(), "bar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
185
src/sql/tables.sql
Normal file
185
src/sql/tables.sql
Normal file
@@ -0,0 +1,185 @@
|
||||
CREATE TABLE config (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keyname TEXT,
|
||||
value TEXT
|
||||
);
|
||||
CREATE INDEX config_index1 ON config (keyname);
|
||||
CREATE TABLE contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT DEFAULT '',
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
origin INTEGER DEFAULT 0,
|
||||
blocked INTEGER DEFAULT 0,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
param TEXT DEFAULT '',
|
||||
authname TEXT DEFAULT '',
|
||||
selfavatar_sent INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
|
||||
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
|
||||
INSERT INTO contacts (id,name,origin) VALUES
|
||||
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144),
|
||||
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144),
|
||||
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);
|
||||
|
||||
CREATE TABLE chats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type INTEGER DEFAULT 0,
|
||||
name TEXT DEFAULT '',
|
||||
draft_timestamp INTEGER DEFAULT 0,
|
||||
draft_txt TEXT DEFAULT '',
|
||||
blocked INTEGER DEFAULT 0,
|
||||
grpid TEXT DEFAULT '',
|
||||
param TEXT DEFAULT '',
|
||||
archived INTEGER DEFAULT 0,
|
||||
gossiped_timestamp INTEGER DEFAULT 0,
|
||||
locations_send_begin INTEGER DEFAULT 0,
|
||||
locations_send_until INTEGER DEFAULT 0,
|
||||
locations_last_sent INTEGER DEFAULT 0,
|
||||
created_timestamp INTEGER DEFAULT 0,
|
||||
muted_until INTEGER DEFAULT 0,
|
||||
ephemeral_timer INTEGER
|
||||
);
|
||||
CREATE INDEX chats_index1 ON chats (grpid);
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
CREATE INDEX chats_index3 ON chats (locations_send_until);
|
||||
INSERT INTO chats (id,type,name) VALUES
|
||||
(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'),
|
||||
(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'),
|
||||
(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd');
|
||||
|
||||
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
|
||||
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
|
||||
|
||||
CREATE TABLE msgs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT DEFAULT '',
|
||||
server_folder TEXT DEFAULT '',
|
||||
server_uid INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0,
|
||||
to_id INTEGER DEFAULT 0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
type INTEGER DEFAULT 0,
|
||||
state INTEGER DEFAULT 0,
|
||||
msgrmsg INTEGER DEFAULT 1,
|
||||
bytes INTEGER DEFAULT 0,
|
||||
txt TEXT DEFAULT '',
|
||||
txt_raw TEXT DEFAULT '',
|
||||
param TEXT DEFAULT '',
|
||||
starred INTEGER DEFAULT 0,
|
||||
timestamp_sent INTEGER DEFAULT 0,
|
||||
timestamp_rcvd INTEGER DEFAULT 0,
|
||||
hidden INTEGER DEFAULT 0,
|
||||
mime_headers TEXT,
|
||||
mime_in_reply_to TEXT,
|
||||
mime_references TEXT,
|
||||
move_state INTEGER DEFAULT 1,
|
||||
location_id INTEGER DEFAULT 0,
|
||||
error TEXT DEFAULT '',
|
||||
|
||||
-- Timer value in seconds. For incoming messages this
|
||||
-- timer starts when message is read, so we want to have
|
||||
-- the value stored here until the timer starts.
|
||||
ephemeral_timer INTEGER DEFAULT 0,
|
||||
|
||||
-- Timestamp indicating when the message should be
|
||||
-- deleted. It is convenient to store it here because UI
|
||||
-- needs this value to display how much time is left until
|
||||
-- the message is deleted.
|
||||
ephemeral_timestamp INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
|
||||
CREATE INDEX msgs_index2 ON msgs (chat_id);
|
||||
CREATE INDEX msgs_index3 ON msgs (timestamp);
|
||||
CREATE INDEX msgs_index4 ON msgs (state);
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);
|
||||
CREATE INDEX msgs_index6 ON msgs (location_id);
|
||||
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id);
|
||||
INSERT INTO msgs (id,msgrmsg,txt) VALUES
|
||||
(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'),
|
||||
(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'),
|
||||
(8,0,'rsvd'), (9,0,'daymarker');
|
||||
|
||||
CREATE TABLE jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
added_timestamp INTEGER,
|
||||
desired_timestamp INTEGER DEFAULT 0,
|
||||
action INTEGER,
|
||||
foreign_id INTEGER,
|
||||
param TEXT DEFAULT '',
|
||||
thread INTEGER DEFAULT 0,
|
||||
tries INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
|
||||
|
||||
CREATE TABLE leftgrps (
|
||||
id INTEGER PRIMARY KEY,
|
||||
grpid TEXT DEFAULT ''
|
||||
);
|
||||
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
|
||||
|
||||
CREATE TABLE keypairs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
private_key,
|
||||
public_key,
|
||||
created INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE acpeerstates (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
last_seen_autocrypt INTEGER DEFAULT 0,
|
||||
public_key,
|
||||
prefer_encrypted INTEGER DEFAULT 0,
|
||||
gossip_timestamp INTEGER DEFAULT 0,
|
||||
gossip_key,
|
||||
public_key_fingerprint TEXT DEFAULT '',
|
||||
gossip_key_fingerprint TEXT DEFAULT '',
|
||||
verified_key,
|
||||
verified_key_fingerprint TEXT DEFAULT ''
|
||||
);
|
||||
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
|
||||
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
|
||||
|
||||
CREATE TABLE msgs_mdns (
|
||||
msg_id INTEGER,
|
||||
contact_id INTEGER,
|
||||
timestamp_sent INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
|
||||
|
||||
CREATE TABLE tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
namespc INTEGER DEFAULT 0,
|
||||
foreign_id INTEGER DEFAULT 0,
|
||||
token TEXT DEFAULT '',
|
||||
timestamp INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL DEFAULT 0.0,
|
||||
longitude REAL DEFAULT 0.0,
|
||||
accuracy REAL DEFAULT 0.0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0,
|
||||
independent INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX locations_index1 ON locations (from_id);
|
||||
CREATE INDEX locations_index2 ON locations (timestamp);
|
||||
|
||||
CREATE TABLE devmsglabels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT,
|
||||
msg_id INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
|
||||
@@ -898,15 +898,15 @@ impl Context {
|
||||
}
|
||||
|
||||
pub(crate) async fn update_device_chats(&self) -> Result<(), Error> {
|
||||
if self.get_config_bool(Config::Bot).await {
|
||||
if self.get_config_bool(Config::Bot).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// create saved-messages chat; we do this only once, if the user has deleted the chat,
|
||||
// he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
|
||||
if !self.sql.get_raw_config_bool(self, "self-chat-added").await {
|
||||
if !self.sql.get_raw_config_bool("self-chat-added").await? {
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "self-chat-added", true)
|
||||
.set_raw_config_bool("self-chat-added", true)
|
||||
.await?;
|
||||
chat::create_by_contact_id(self, DC_CONTACT_ID_SELF).await?;
|
||||
}
|
||||
@@ -1061,10 +1061,16 @@ mod tests {
|
||||
};
|
||||
|
||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len();
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
self_talk_id.delete(&t).await.ok();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(),
|
||||
chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
device_chat_msgs_before + 1
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use async_std::{channel, pin::Pin};
|
||||
use async_std::{future::Future, task};
|
||||
use chat::ChatItem;
|
||||
use once_cell::sync::Lazy;
|
||||
use sqlx::Row;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
@@ -85,6 +86,7 @@ impl TestContext {
|
||||
|
||||
async fn new_named(name: Option<String>) -> Self {
|
||||
use rand::Rng;
|
||||
pretty_env_logger::try_init().ok();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
@@ -95,9 +97,10 @@ impl TestContext {
|
||||
}
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
.expect("failed to create context");
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
|
||||
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
let sinks = Arc::clone(&event_sinks);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
@@ -114,6 +117,7 @@ impl TestContext {
|
||||
|
||||
while let Some(event) = events.recv().await {
|
||||
{
|
||||
log::debug!("{:?}", event);
|
||||
let sinks = sinks.read().await;
|
||||
for sink in sinks.iter() {
|
||||
sink(event.clone()).await;
|
||||
@@ -224,22 +228,25 @@ impl TestContext {
|
||||
let row = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
.fetch_one(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT id, foreign_id, param
|
||||
FROM jobs
|
||||
WHERE action=?
|
||||
ORDER BY desired_timestamp DESC;
|
||||
"#,
|
||||
paramsv![Action::SendMsgToSmtp],
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let foreign_id: i64 = row.get(1)?;
|
||||
let param: String = row.get(2)?;
|
||||
Ok((id, foreign_id, param))
|
||||
},
|
||||
)
|
||||
.bind(Action::SendMsgToSmtp),
|
||||
)
|
||||
.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 {
|
||||
break row;
|
||||
}
|
||||
@@ -249,7 +256,7 @@ impl TestContext {
|
||||
panic!("no sent message found in jobs table");
|
||||
}
|
||||
};
|
||||
let id = MsgId::new(foreign_id as u32);
|
||||
let id = MsgId::new(foreign_id);
|
||||
let params = Params::from_str(&raw_params).unwrap();
|
||||
let blob_path = params
|
||||
.get_blob(Param::File, &self.ctx, false)
|
||||
@@ -259,7 +266,7 @@ impl TestContext {
|
||||
.to_abs_path();
|
||||
self.ctx
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
||||
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(rowid))
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
||||
@@ -302,7 +309,9 @@ impl TestContext {
|
||||
///
|
||||
/// Panics on errors or if the most recent message is a marker.
|
||||
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
@@ -313,13 +322,17 @@ impl TestContext {
|
||||
|
||||
/// Gets the most recent message over all chats.
|
||||
pub async fn get_last_msg(&self) -> Message {
|
||||
let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&self.ctx, 0, None, None)
|
||||
.await
|
||||
.expect("failed to load chatlist");
|
||||
// 0 is correct in the next line (as opposed to `chats.len() - 1`, which would be the last element):
|
||||
// The chatlist describes what you see when you open DC, a list of chats and in each of them
|
||||
// the first words of the last message. To get the last message overall, we look at the chat at the top of the
|
||||
// list, which has the index 0.
|
||||
let msg_id = chats.get_msg_id(0).unwrap();
|
||||
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
|
||||
Message::load_from_db(&self.ctx, msg_id)
|
||||
.await
|
||||
.expect("failed to load msg")
|
||||
}
|
||||
|
||||
/// Creates or returns an existing 1:1 [`Chat`] with another account.
|
||||
@@ -333,8 +346,14 @@ impl TestContext {
|
||||
.ctx
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default(),
|
||||
other.ctx.get_config(Config::ConfiguredAddr).await.unwrap(),
|
||||
other
|
||||
.ctx
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
@@ -394,7 +413,7 @@ impl TestContext {
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::clippy::indexing_slicing)]
|
||||
pub async fn print_chat(&self, chat_id: ChatId) {
|
||||
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await;
|
||||
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await.unwrap();
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
@@ -405,7 +424,7 @@ impl TestContext {
|
||||
.collect();
|
||||
|
||||
let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap();
|
||||
let members = chat::get_chat_contacts(self, sel_chat.id).await;
|
||||
let members = chat::get_chat_contacts(self, sel_chat.id).await.unwrap();
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
@@ -428,7 +447,7 @@ impl TestContext {
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(self).await {
|
||||
match sel_chat.get_profile_image(self).await.unwrap() {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
@@ -481,7 +500,7 @@ impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
if !thread::panicking() {
|
||||
if let Ok(p) = self.poison_receiver.try_recv() {
|
||||
panic!(p);
|
||||
panic!("{}", p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,7 +582,7 @@ pub(crate) async fn get_chat_msg(
|
||||
index: usize,
|
||||
asserted_msgs_count: usize,
|
||||
) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
assert_eq!(msgs.len(), asserted_msgs_count);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
|
||||
msg_id
|
||||
|
||||
69
src/token.rs
69
src/token.rs
@@ -4,17 +4,13 @@
|
||||
//!
|
||||
//! Tokens are used in countermitm verification protocols.
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_create_id, time};
|
||||
|
||||
/// Token namespace
|
||||
#[derive(
|
||||
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(u32)]
|
||||
pub enum Namespace {
|
||||
Unknown = 0,
|
||||
Auth = 110,
|
||||
@@ -30,26 +26,36 @@ impl Default for Namespace {
|
||||
/// Creates a new token and saves it into the database.
|
||||
///
|
||||
/// Returns created token.
|
||||
pub async fn save(context: &Context, namespace: Namespace, chat: Option<ChatId>) -> String {
|
||||
pub async fn save(context: &Context, namespace: Namespace, foreign_id: Option<ChatId>) -> String {
|
||||
let token = dc_create_id();
|
||||
match chat {
|
||||
Some(chat_id) => context
|
||||
match foreign_id {
|
||||
Some(foreign_id) => context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||
paramsv![namespace, chat_id, token, time()],
|
||||
sqlx::query(
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);"
|
||||
)
|
||||
.bind(namespace)
|
||||
.bind(foreign_id)
|
||||
.bind(&token)
|
||||
.bind(time()),
|
||||
)
|
||||
.await
|
||||
.ok(),
|
||||
None => context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);",
|
||||
paramsv![namespace, token, time()],
|
||||
sqlx::query(
|
||||
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);"
|
||||
)
|
||||
.bind(namespace)
|
||||
.bind(&token)
|
||||
.bind(time()),
|
||||
)
|
||||
.await
|
||||
.ok(),
|
||||
};
|
||||
|
||||
token
|
||||
}
|
||||
|
||||
@@ -57,50 +63,51 @@ pub async fn lookup(
|
||||
context: &Context,
|
||||
namespace: Namespace,
|
||||
chat: Option<ChatId>,
|
||||
) -> Option<String> {
|
||||
match chat {
|
||||
) -> crate::sql::Result<Option<String>> {
|
||||
let token = match chat {
|
||||
Some(chat_id) => {
|
||||
context
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
context,
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
|
||||
paramsv![namespace, chat_id],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;")
|
||||
.bind(namespace)
|
||||
.bind(chat_id),
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
// foreign_id is declared as `INTEGER DEFAULT 0` in the schema.
|
||||
None => {
|
||||
context
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
context,
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;",
|
||||
paramsv![namespace],
|
||||
.query_get_value(
|
||||
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;")
|
||||
.bind(namespace),
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub async fn lookup_or_new(
|
||||
context: &Context,
|
||||
namespace: Namespace,
|
||||
chat: Option<ChatId>,
|
||||
foreign_id: Option<ChatId>,
|
||||
) -> String {
|
||||
if let Some(token) = lookup(context, namespace, chat).await {
|
||||
if let Ok(Some(token)) = lookup(context, namespace, foreign_id).await {
|
||||
return token;
|
||||
}
|
||||
|
||||
save(context, namespace, chat).await
|
||||
save(context, namespace, foreign_id).await
|
||||
}
|
||||
|
||||
pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
|
||||
paramsv![namespace, token],
|
||||
sqlx::query("SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;")
|
||||
.bind(namespace)
|
||||
.bind(token),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -11,7 +11,7 @@ Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.
|
||||
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
|
||||
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
|
||||
Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749))
|
||||
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)) and Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847))
|
||||
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
|
||||
Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
|
||||
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-over-email-specification)
|
||||
Detect mailing list | List-Id ([RFC 2919](https://tools.ietf.org/html/rfc2919)) and Precedence ([RFC 3834](https://tools.ietf.org/html/rfc3834))
|
||||
|
||||
@@ -20,7 +20,7 @@ MIME-Version: 1.0
|
||||
Date: Tue, 24 Nov 2020 09:34:48 +0000
|
||||
Chat-Version: 1.0
|
||||
Autocrypt-Setup-Message: v1
|
||||
Message-ID: <Mr._G2UTiTkgfk.HWf5RnFC2xy@testrun.org>
|
||||
Message-ID: <abc@example.com>
|
||||
To: <alice@example.com>
|
||||
From: <alice@example.com>
|
||||
Content-Type: multipart/mixed; boundary="dKhu3bbmBniQsT8W8w58YRCCiBK2YY"
|
||||
|
||||
46
test-data/message/allinkl-quote.eml
Normal file
46
test-data/message/allinkl-quote.eml
Normal file
@@ -0,0 +1,46 @@
|
||||
Return-Path: <alice@example.org>
|
||||
Delivered-To: bob@example.org
|
||||
Received: from hq5.merlinux.eu
|
||||
by hq5.merlinux.eu with LMTP
|
||||
id eHU/Co4EUmBAQQAAPzvFDg
|
||||
(envelope-from <alice@example.org>)
|
||||
for <bob@example.org>; Wed, 17 Mar 2021 14:30:54 +0100
|
||||
Received: from dd37930.kasserver.com (dd37930.kasserver.com [85.13.154.127])
|
||||
by hq5.merlinux.eu (Postfix) with ESMTPS id CB5D927A0071
|
||||
for <bob@example.org>; Wed, 17 Mar 2021 14:30:53 +0100 (CET)
|
||||
Received: from dd37930.kasserver.com (dd0805.kasserver.com [85.13.161.253])
|
||||
by dd37930.kasserver.com (Postfix) with ESMTPSA id 724E853C0979
|
||||
for <bob@example.org>; Wed, 17 Mar 2021 14:30:53 +0100 (CET)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/html; charset=ISO-8859-1
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
X-SenderIP: 217.80.3.233
|
||||
User-Agent: ALL-INKL Webmail 2.11
|
||||
In-Reply-To: <Mr.nru4puZrBpw.JfbybhIh75A@testrun.org>
|
||||
References: <Mr.nru4puZrBpw.JfbybhIh75A@testrun.org><Mr.nru4puZrBpw.JfbybhIh75A@testrun.org>
|
||||
Subject: Re: Message from Hocuri
|
||||
From: alice@example.org
|
||||
To: bob@example.org
|
||||
Message-Id: <20210317133053.724E853C0979@dd37930.kasserver.com>
|
||||
Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www=
|
||||
=2Ew3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html lang=3D"de" xml:lang=
|
||||
=3D"en" xmlns=3D"http://www.w3.org/1999/xhtml"><head><meta http-equiv=3D"Co=
|
||||
ntent-Type" content=3D"text/html; charset=3DISO-8859-1" /><title></title><s=
|
||||
tyle type=3D"text/css">html,body{background-color:#fff;color:#333;line-heig=
|
||||
ht:1.4;font-family:sans-serif,Arial,Verdana,Trebuchet MS;}</style></head><b=
|
||||
ody><p>It's 1.0.</p>
|
||||
<div ></div>
|
||||
<p>Hocuri schrieb am 17.03.2021 14:25 (GMT +01:00):</p>
|
||||
<blockquote cite=3D"mid:Mr.nru4puZrBpw.JfbybhIh75A@testrun.org">
|
||||
<pre>What's the version?
|
||||
|
||||
--=20
|
||||
Sent with my Delta Chat Messenger: <a href=3D"https://delta.chat" target=3D=
|
||||
"_blank" rel=3D"nofollow noopener" title=3D"https://delta.chat">https://del=
|
||||
ta.chat</a>
|
||||
|
||||
|
||||
</pre>
|
||||
</blockquote></body></html>
|
||||
36
test-data/message/protonmail-mixed-up.eml
Normal file
36
test-data/message/protonmail-mixed-up.eml
Normal file
@@ -0,0 +1,36 @@
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: bob@example.org
|
||||
Date: Mon, 29 Mar 2021 11:30:57 +0000
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
Reply-To: Alice <alice@example.com>
|
||||
Subject: ...
|
||||
Message-ID: <Mr.AkmaxDNOYj0.oNPtoFR8EHC@example.com>
|
||||
In-Reply-To: <Mr.Y1EWG9-FLhN.KUZ4cu74MYR@example.org>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Empty Message
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0
|
||||
Content-Type: application/pgp-encrypted; name=attachment.pgp
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=attachment.pgp
|
||||
|
||||
VmVyc2lvbjogMQ0KDQo=
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0
|
||||
Content-Type: application/octet-stream; name=encrypted.asc
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=encrypted.asc
|
||||
|
||||
UEdQIFBBWUxPQUQgV0FTIEhFUkUK
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0--
|
||||
|
||||
31
test-data/message/protonmail-repaired.eml
Normal file
31
test-data/message/protonmail-repaired.eml
Normal file
@@ -0,0 +1,31 @@
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: bob@example.org
|
||||
Date: Mon, 29 Mar 2021 11:30:57 +0000
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
Reply-To: Alice <alice@example.com>
|
||||
Subject: ...
|
||||
Message-ID: <Mr.AkmaxDNOYj0.oNPtoFR8EHC@example.com>
|
||||
In-Reply-To: <Mr.Y1EWG9-FLhN.KUZ4cu74MYR@example.org>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0"
|
||||
X-Enigmail-Info: Fixed broken PGP/MIME message
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0
|
||||
Content-Type: application/pgp-encrypted; name=attachment.pgp
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=attachment.pgp
|
||||
|
||||
VmVyc2lvbjogMQ0KDQo=
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0
|
||||
Content-Type: application/octet-stream; name=encrypted.asc
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=encrypted.asc
|
||||
|
||||
UEdQIFBBWUxPQUQgV0FTIEhFUkUK
|
||||
|
||||
--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0--
|
||||
|
||||
Reference in New Issue
Block a user