Compare commits

...

51 Commits

Author SHA1 Message Date
B. Petersen
cc6b02f037 remove additional tags: check from ci, it is 'master branch || has tags' so that does not make much sense, generation should be done on master only 2021-04-14 16:55:45 +02:00
B. Petersen
e13bb8fbd4 bump version to 1.52.0 2021-04-14 16:55:45 +02:00
B. Petersen
eaca4446aa update changelog to 1.52 2021-04-14 16:55:45 +02:00
B. Petersen
f3fb26c066 add a test to search for one-to-one-chats coming without authnames 2021-04-14 14:09:35 +02:00
B. Petersen
1a1416e446 change chat names correctly on contact name change
the user-given contact name may be set to an empty string;
in this case the authname or the email is used for the contact
and also for the name of possibly existing chats.

this works well for `dc_chat_get_name()` as that just uses
`dc_contact_get_display_name()` for single-chats.

it did not work for `dc_get_chatlist(query)` as that
uses the database for performance reasons -
however, in the database, the empty string is written
instead of the display name is written for a chat.

this is fixed by this pr by also using
dc_contact_get_display_name() when updating the chats-table
(similar to `dc_create_chat_by_contact_id()`)
2021-04-14 14:09:35 +02:00
B. Petersen
0afc07f6e7 add a test to search for one-to-one-chats 2021-04-14 14:09:35 +02:00
Asiel Díaz Benítez
dda3c605c6 Merge pull request #2326 from deltachat/adb-fix-message-id
make Message.id a dynamic property
2021-04-13 04:08:50 -04:00
link2xt
d4e065ee84 Update once_cell, base64, itertools, strum and strum_macros 2021-04-13 02:18:15 +03:00
link2xt
bc222af661 Cargo.toml: sort dependencies alphabetically
This is what we do in rPGP too.
2021-04-13 02:18:15 +03:00
adbenitez
f6136f0ecc fix Chat.prepare_message() 2021-04-12 14:59:15 -04:00
adbenitez
b2517d3060 make Message.id a property 2021-04-12 14:59:15 -04:00
link2xt
244260a978 Fix nightly clippy and rustc errors 2021-04-12 21:33:52 +03:00
Asiel Díaz Benítez
f17320a9cb Merge pull request #2332 from deltachat/adb-issue-2327
Add sticker viewtype
2021-04-11 20:49:49 -04:00
adbenitez
d1237c9f8d add Message.is_sticker() 2021-04-11 20:37:47 -04:00
adbenitez
b9beaee7d4 update Message.new_empty() to allow view_type="sticker" and also allow using
message type directly (ex. `const.DC_MSG_STICKER`) so if new message types are
added, they can be used direcly without needing the python API to be updated.
2021-04-11 20:37:47 -04:00
adbenitez
258856c23a add sticker type 2021-04-11 20:37:47 -04:00
adbenitez
72ddd33adf avoid for loop 2021-04-11 20:37:47 -04:00
link2xt
1cd53aafff Add support for "Mixed Up" MIME format
This is an PGP/MIME format produced by Microsoft Exchange and ProtonMail IMAP/SMTP Bridge,
described in detail in https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html

This patch adds seamless support for "Mixed Up" Encryption, repairing
mangled Autocrypt messages without notifying the user.
2021-04-11 19:50:59 +03:00
link2xt
4d2ac5a3a2 ci: switch to v2 of actions/checkout 2021-04-11 14:45:27 +03:00
link2xt
146db48c35 ci: use DCC_NEW_TMP_EMAIL for remote python tests 2021-04-11 14:45:27 +03:00
link2xt
9529d76d82 ci: update ci_scripts README 2021-04-11 14:45:27 +03:00
link2xt
b5f2752e41 python: remove DCC_PY_LIVECONFIG references from all scripts
This variable is not used anymore.
2021-04-11 14:45:27 +03:00
link2xt
ce4675e9f7 ci: move remote python tests from CircleCI to GitHub Actions 2021-04-11 14:45:27 +03:00
link2xt
f0bd129636 ci: fix syntax of git --format in run-doxygen.sh
git version 2.31.0 throws fatal error on --format without "="
2021-04-11 14:45:27 +03:00
link2xt
dfe3cabb14 circleci: remove remote_tests_rust
Rust tests are already running on GitHub Actions, this is duplicate work.
2021-04-11 14:45:27 +03:00
link2xt
09735b808e circleci: remove unused jobs 2021-04-11 14:45:27 +03:00
link2xt
37f68459f6 sql: make all queries persistent and update to upstream sqlx
&str queries are not persistent by default.  To make queries persistent,
they have to be constructed with sqlx::query.

Upstream sqlx does not contain the change that make all queries
persistent, but it is not needed anymore. but
2021-04-10 22:24:12 +02:00
Hocuri
3707471266 Add alias support 2 (#2297)
fix  #2073
fix #2292 (I think)

- Messages can be assigned to any chat by the References and In-Reply-To, also to 1:1 chats; this has higher priority than the group id because with ad-hoc groups, it can happen that two devices have different group ids for the same conversation thread.
- If `From` is not in the chat (we call this "shadow sender"), `OverrideSenderDisplayname` is set. This communicates to the UI that:
  - A `~`should be added in front of the sender's displayname.
  - Also in 1:1 chats, the sender's displayname and avatar is shown, as if this was a group.

  The "Unknown sender for this chat" messages are completely removed for unprotected groups.

For protected chats, everything stays as it was before.

POSTPONED:

- Maybe (if it turns out to be still necessary):
  - The ad-hoc group id is computed by the the References, instead of the member list, as it is currently done
  - How do we prevent that the message can't be assigned to the correct chat as the parent message was deleted?
2021-04-10 22:06:22 +02:00
Hocuri
5394327bf6 More logging for "core spams imap events"
TODO: revert
2021-04-10 17:08:41 +03:00
Hocuri
df277b374d Ignore unknown classical emails from spam folder (#2311) 2021-04-10 10:45:47 +02:00
link2xt
53dba3c1ba Merge in sqlx fixes 2021-04-08 22:54:58 +03:00
link2xt
6540ee60e5 Update Cargo.lock 2021-04-08 22:05:15 +03:00
link2xt
66b5084a1d Switch to /deltachat/ org fork of sqlx 2021-04-08 21:58:04 +03:00
link2xt
f76aaf3205 sql: enable virtual statement cache on the reader pool
A follow-up to 720135a915
2021-04-07 21:43:34 +03:00
Hocuri
179a2a50e6 Parse <blockquote> tags for better quote detection (#2313) 2021-04-07 18:45:00 +02:00
link2xt
720135a915 Update sqlx to enable statement cache 2021-04-07 12:41:23 +03:00
Friedel Ziegelmayer
6bb5721f29 feat: improve internal sql interface
Switches from rusqlite to sqlx to have a fully async based interface
to sqlite.

Co-authored-by: B. Petersen <r10s@b44t.com>
Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: link2xt <link2xt@testrun.org>
2021-04-06 16:06:11 +02:00
link2xt
4dedc2d8ce Fix a comment typo 2021-03-27 21:11:34 +03:00
link2xt
ede9bdc018 Reduce required cmake version to 3.16 2021-03-27 00:17:04 +03:00
holger krekel
11823d3b45 use master for tag-buids of upload wheels job 2021-03-23 22:20:14 +01:00
missytake
734ea8ab1b Merge pull request #2314 from deltachat/py51release
prepare 1.51.0 release
2021-03-23 19:42:39 +01:00
holger krekel
7017a050cb prepare 1.51.0 release 2021-03-23 18:55:34 +01:00
B. Petersen
96e57e7ef3 bump version to 1.51.0 2021-03-23 18:51:26 +01:00
B. Petersen
02bc334af5 update changelog for 1.51 2021-03-23 18:51:26 +01:00
Simon Laux
c8fea9c577 Merge pull request #2303 from deltachat/add_cmake_build_to_gitignore
add /build directory to .gitignore
2021-03-21 17:12:03 +01:00
link2xt
cdc1063d83 Do not reset user status after receiving a read receipt
Read receipts never contain the signature, so previously receiving it
cleared the status.
2021-03-21 18:54:08 +03:00
Simon Laux
704a902cc5 add build directory to gitignore
(libdeltachat generated with cmake)
2021-03-20 18:23:30 +01:00
B. Petersen
36aef6499d update provider database 2021-03-18 21:55:33 +01:00
B. Petersen
4ba9c2fafa fix clippy error on generating rust code from python 2021-03-18 21:55:33 +01:00
Hocuri
0de8b6a7e5 Update uid_next if the server rewinded it
fix #2188

Also, if we notice that the server started reusing old UIDs, _also_ do a `ResyncFolders`, because the server likely forgot to change uid_validity
2021-03-18 16:14:56 +03:00
link2xt
04f816be31 qr: return QrFprMismatch on fingerprint mismatch
Previously QrFprWithoutAddr was returned incorrectly.

Also fix spelling error ("Missmatch").
2021-03-15 21:39:10 +03:00
86 changed files with 7127 additions and 5691 deletions

View File

@@ -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

View File

@@ -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
View 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
View File

@@ -1,5 +1,6 @@
/target
**/*.rs.bk
/build
# ignore vi temporaries
*~

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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);

View File

@@ -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()");

View File

@@ -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

View File

@@ -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"

View File

@@ -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()
}

View File

@@ -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" => {

View File

@@ -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() {

View File

@@ -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
------

View File

@@ -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.

View File

@@ -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())))
#

View File

@@ -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")

View File

@@ -13,7 +13,6 @@ passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER

View File

@@ -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

View File

@@ -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()
);
}

View File

@@ -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)]

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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();

View File

@@ -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");
}
}

View File

@@ -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 {

View File

@@ -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, &param.addr, &param.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(&param_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?;

View File

@@ -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(),
}],
);

View File

@@ -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,

View File

@@ -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))

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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,
_ => {}
}
}

View File

@@ -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(())
}
}

View File

@@ -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);
}
}
}

View File

@@ -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) => ({

View File

@@ -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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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"));
}
}

View File

@@ -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))
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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()
};

View File

@@ -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]

View File

@@ -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;

View File

@@ -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(())

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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
View File

@@ -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"
);
}

View File

@@ -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 };

View File

@@ -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")));
}

View File

@@ -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),

View File

@@ -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),
};

View File

@@ -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

File diff suppressed because it is too large Load Diff

19
src/sql/error.rs Normal file
View 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
View 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
View 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
View 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);

View File

@@ -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
);

View File

@@ -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

View File

@@ -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()

View File

@@ -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))

View File

@@ -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"

View 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>

View 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--

View 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--