diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 90bf4fdc3..a05066102 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2020-03-12 + toolchain: nightly-2020-03-19 override: true - uses: actions-rs/cargo@v1 with: @@ -25,7 +25,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2020-03-12 + toolchain: nightly-2020-03-19 override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2020-03-12 + toolchain: nightly-2020-03-19 components: clippy override: true - uses: actions-rs/clippy-check@v1 diff --git a/ASYNC-API-TODO.txt b/ASYNC-API-TODO.txt new file mode 100644 index 000000000..4bdf14092 --- /dev/null +++ b/ASYNC-API-TODO.txt @@ -0,0 +1,54 @@ + +Delta Chat ASYNC (friedel, bjoern, floris, friedel) + +- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht) + +APIs: + dc_context_new # opens the database + dc_open # FFI only + -> drop it and move parameters to dc_context_new() + + dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently + dc_imex NEVER goes through the job system + dc_imex import_backup needs to ensure dc_stop_jobs() + + dc_start_io # start smtp/imap and job handling subsystems + dc_stop_io # stop smtp/imap and job handling subsystems + dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running + + dc_close # FFI only + -> can be dropped + dc_context_unref + + for ios share-extension: + Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure + 0: message was sent + 1: message failed to go out, is queued as a job to be retried later + 2: message permanently failed? + +EVENT handling: + start a callback thread and call get_next_event() which is BLOCKING + it's fine to start this callback thread later, it will see all events. + Note that the core infinitely fills the internal queue if you never drain it. + + FFI-get_next_event() returns NULL if the context is unrefed already? + + sidenote: how python's callback thread does it currently: + CB-thread runs this while loop: + while not QUITFLAG: + ev = context.get_next_event( ) + ... + So in order to shutdown properly one has to set QUITFLAG + before calling dc_stop_jobs() and dc_context_unref + + event API: + get_data1_int + get_data2_int + get_data3_str + + +- userdata likely only used for the callbacks, likely can be dropped, needs verification + + +- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going + diff --git a/Cargo.lock b/Cargo.lock index 2430a2cec..d9ef44466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,806 +1,896 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543" +dependencies = [ + "gimli", +] + [[package]] name = "adler32" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" [[package]] name = "aes" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" dependencies = [ - "aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "aes-soft", + "aesni", + "block-cipher-trait", ] [[package]] name = "aes-soft" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "byteorder", + "opaque-debug", ] [[package]] name = "aesni" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "opaque-debug", ] [[package]] name = "aho-corasick" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", ] [[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", ] [[package]] name = "anyhow" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" [[package]] name = "arrayref" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" dependencies = [ - "nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "nodrop", ] [[package]] name = "arrayvec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] name = "ascii_utils" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "async-attributes" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423" dependencies = [ - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6", + "syn 1.0.23", +] + +[[package]] +name = "async-h1" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd501febce09534b924aa471e6a7fd689071fee63659473413f62a1979ae56" +dependencies = [ + "async-std", + "byte-pool", + "futures-core", + "http-types", + "httparse", + "lazy_static", + "log", + "pin-project-lite", + "url", ] [[package]] name = "async-imap" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8113a1ae7ed80ce764f7474f4323b66f945367c2195d046cfa5efdde30fca04c" dependencies = [ - "async-attributes 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rental 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "stop-token 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "async-native-tls", + "async-std", + "base64 0.11.0", + "byte-pool", + "chrono", + "futures 0.3.5", + "imap-proto", + "lazy_static", + "log", + "nom 5.1.1", + "pin-utils", + "rental", + "stop-token", + "thiserror", ] [[package]] name = "async-native-tls" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33" dependencies = [ - "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "async-std", + "native-tls", + "thiserror", + "url", ] [[package]] name = "async-smtp" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb010dac8f81ceb798b089c522766c0427b54253789194b5c7de9720aeb7f091" dependencies = [ - "async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "async-trait 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bufstream 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", - "hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "snafu 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "async-native-tls", + "async-std", + "async-trait", + "base64 0.11.0", + "bufstream", + "fast_chemail", + "hostname", + "log", + "nom 5.1.1", + "pin-project", + "pin-utils", + "serde", + "serde_derive", + "serde_json", + "thiserror", ] [[package]] name = "async-std" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45cee2749d880d7066e328a7e161c7470ced883b2fd000ca4643e9f1dd5083a" dependencies = [ - "async-task 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "broadcaster 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-timer 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "kv-log-macro 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "async-attributes", + "async-task", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-timer", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "smol", + "wasm-bindgen-futures", ] [[package]] name = "async-task" -version = "1.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" [[package]] name = "async-trait" -version = "0.1.24" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", + "winapi", ] [[package]] name = "autocfg" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" [[package]] name = "autocfg" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.44" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130" dependencies = [ - "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "addr2line", + "cfg-if", + "libc", + "object", + "rustc-demangle", ] [[package]] name = "base64" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", ] [[package]] name = "base64" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "bit-set" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" dependencies = [ - "bit-vec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bit-vec", ] [[package]] name = "bit-vec" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0dc55f2d8a1a85650ac47858bb001b4c0dd73d79e3c455a842925e68d29cd3" [[package]] name = "bitfield" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "blake2b_simd" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" dependencies = [ - "arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayref", + "arrayvec 0.5.1", + "constant_time_eq", ] [[package]] name = "block-buffer" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-padding", + "byte-tools", + "byteorder", + "generic-array", ] [[package]] name = "block-cipher-trait" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "block-modes" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa8410095e39fdb732909fb5730a48d5bd7c2e3cd76bd1b07b3dbea130c529" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "block-padding", ] [[package]] name = "block-padding" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" dependencies = [ - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools", ] [[package]] name = "blowfish" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "broadcaster" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "byteorder", + "opaque-debug", ] [[package]] name = "buf_redux" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", + "safemem", ] [[package]] name = "bufstream" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5356f1d23ee24a1f785a56d1d1a5f0fd5b0f6a0c0fb2412ce11da71649ab78f6" [[package]] name = "byte-pool" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9342e102eac8b1879fbedf9a7e0572c40b0cc5805b663c4d4ca791cae0bae221" dependencies = [ - "crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-queue", + "stable_deref_trait", ] [[package]] name = "byte-tools" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytecount" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f" [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] [[package]] name = "bytes" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "c2-chacha" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" [[package]] name = "cargo_metadata" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe" dependencies = [ - "error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "error-chain", + "semver", + "serde", + "serde_derive", + "serde_json", ] [[package]] name = "cast5" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce5759b4c52ca74f9a98421817c882f1fd9b0071ae41cd61ab9f9d059c04fd6" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "byteorder", + "opaque-debug", ] [[package]] name = "cc" -version = "1.0.50" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" [[package]] name = "cfb-mode" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190e7b55d3a27cf8879becf61035a141cbc783f3258a41d16d1706719f991345" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "stream-cipher", ] [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "charset" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.10.1", + "encoding_rs", ] [[package]] name = "chrono" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer", + "num-traits", + "time", ] [[package]] name = "circular" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" [[package]] name = "clear_on_drop" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", ] [[package]] name = "cloudabi" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", ] [[package]] name = "color_quant" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" +dependencies = [ + "time", +] [[package]] name = "core-foundation" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" dependencies = [ - "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] [[package]] name = "core-foundation-sys" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "crc24" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" [[package]] name = "crc32fast" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-channel" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "crossbeam-deque" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" dependencies = [ - "crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", ] [[package]] name = "crossbeam-epoch" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "cfg-if", + "lazy_static", ] [[package]] name = "ctor" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf6b25ee9ac1995c54d7adb2eff8cfffb7260bc774fb63c601ec65467f43cd9d" dependencies = [ - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "curve25519-dalek" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "digest", + "rand_core 0.5.1", + "subtle", + "zeroize", ] [[package]] name = "darling" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.6", + "strsim", + "syn 1.0.23", ] [[package]] name = "darling_macro" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_core", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "debug_stub_derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496b7f8a2f853313c3ca370641d7ff3e42c32974fdccda8f0684599ed0a3ff6b" dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15", + "syn 0.11.11", ] [[package]] name = "deflate" version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "adler32", + "byteorder", ] [[package]] name = "deltachat" version = "1.33.0" dependencies = [ - "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", - "async-imap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "async-smtp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "deltachat_derive 2.0.0", - "email 0.0.21 (git+https://github.com/deltachat/rust-email)", - "encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)", - "escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "image 0.22.5 (registry+https://github.com/rust-lang/crates.io-index)", - "image-meta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "mailparse 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pgp 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proptest 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)", - "r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", - "r2d2_sqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustyline 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "stop-token 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term 0.12.1", + "anyhow", + "async-imap", + "async-native-tls", + "async-smtp", + "async-std", + "async-trait", + "backtrace", + "base64 0.11.0", + "bitflags", + "byteorder", + "charset", + "chrono", + "debug_stub_derive", + "deltachat_derive", + "email", + "encoded-words", + "escaper", + "futures 0.3.5", + "hex", + "image", + "image-meta", + "indexmap", + "itertools", + "lazy_static", + "lettre_email", + "libc", + "log", + "mailparse", + "native-tls", + "num-derive", + "num-traits", + "percent-encoding", + "pgp", + "pretty_assertions", + "pretty_env_logger", + "proptest", + "quick-xml", + "r2d2", + "r2d2_sqlite", + "rand 0.7.3", + "regex", + "rusqlite", + "rustyline", + "sanitize-filename", + "serde", + "serde_json", + "sha2", + "smallvec", + "smol", + "stop-token", + "strum", + "strum_macros", + "surf", + "tempfile", + "thiserror", + "url", ] [[package]] name = "deltachat_derive" version = "2.0.0" dependencies = [ - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "deltachat_ffi" version = "1.33.0" dependencies = [ - "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", - "deltachat 1.33.0", - "human-panic 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "async-std", + "deltachat", + "human-panic", + "libc", + "num-traits", + "serde_json", + "thiserror", ] [[package]] name = "derive_builder" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" dependencies = [ - "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "darling", + "derive_builder_core", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "derive_builder_core" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" dependencies = [ - "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "darling", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "des" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "byteorder", + "opaque-debug", ] [[package]] name = "difference" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" [[package]] name = "digest" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "dirs" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_users", + "winapi", ] -[[package]] -name = "doc-comment" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "dtoa" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" [[package]] name = "ed25519-dalek" version = "1.0.0-pre.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2" dependencies = [ - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clear_on_drop", + "curve25519-dalek", + "rand 0.7.3", + "sha2", ] [[package]] name = "either" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" [[package]] name = "email" version = "0.0.21" source = "git+https://github.com/deltachat/rust-email#ace12ee6f8e054dd890589f588d0311604fc25f0" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)", - "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0", + "chrono", + "encoded-words", + "encoding", + "lazy_static", + "rand 0.7.3", + "time", + "version_check 0.9.2", ] [[package]] @@ -808,581 +898,636 @@ name = "encoded-words" version = "0.1.0" source = "git+https://github.com/async-email/encoded-words#2631c258183620f6d976abffabbfc2dcc697d793" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0", + "charset", + "encoding_rs", + "hex", + "lazy_static", + "regex", + "thiserror", ] [[package]] name = "encoding" version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" dependencies = [ - "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", ] [[package]] name = "encoding-index-japanese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" dependencies = [ - "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_index_tests", ] [[package]] name = "encoding-index-korean" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" dependencies = [ - "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_index_tests", ] [[package]] name = "encoding-index-simpchinese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" dependencies = [ - "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_index_tests", ] [[package]] name = "encoding-index-singlebyte" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" dependencies = [ - "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_index_tests", ] [[package]] name = "encoding-index-tradchinese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" dependencies = [ - "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_index_tests", ] [[package]] name = "encoding_index_tests" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "entities" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "env_logger" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" dependencies = [ - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "humantime", + "log", + "regex", + "termcolor", ] [[package]] name = "error-chain" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" dependencies = [ - "backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace", + "version_check 0.9.2", ] [[package]] name = "escaper" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39da344028c2227132b2dfa7c186e2104ecc153467583d00ed9c398f9ff693b0" dependencies = [ - "entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "entities", ] [[package]] name = "failure" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" dependencies = [ - "backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace", + "failure_derive", ] [[package]] name = "failure_derive" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", + "synstructure", ] [[package]] name = "fake-simd" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fast_chemail" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" dependencies = [ - "ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ascii_utils", ] [[package]] name = "flate2" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", ] [[package]] name = "fnv" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" -version = "0.3.4" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" + +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" dependencies = [ - "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] name = "futures-channel" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" dependencies = [ - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" [[package]] name = "futures-executor" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" dependencies = [ - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core", + "futures-task", + "futures-util", ] [[package]] name = "futures-io" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" [[package]] name = "futures-macro" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" dependencies = [ - "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "futures-sink" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" [[package]] name = "futures-task" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] [[package]] name = "futures-timer" -version = "2.0.2" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper", +] [[package]] name = "futures-util" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" dependencies = [ - "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", + "tokio-io", ] [[package]] name = "generic-array" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" dependencies = [ - "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "typenum", ] [[package]] name = "getrandom" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "wasi", ] [[package]] name = "gif" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" dependencies = [ - "color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "color_quant", + "lzw", ] +[[package]] +name = "gimli" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" + [[package]] name = "glob" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" [[package]] -name = "h2" -version = "0.2.2" +name = "gloo-timers" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] name = "heck" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" dependencies = [ - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.8" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "hex" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" [[package]] name = "hostname" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "winutil", ] [[package]] name = "http" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.4", + "fnv", + "itoa", ] [[package]] -name = "http-body" -version = "0.3.1" +name = "http-client" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271fae45413daaca82e8c6bf46e452b897873676ce56196d8939645711f542d5" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "async-h1", + "async-native-tls", + "async-std", + "futures 0.3.5", + "http-types", + "js-sys", + "log", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "http-types" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d9f44462c2e59d5d5826e7ba74b121ee2fb0ff091ef849692694f1d77aaf50" +dependencies = [ + "anyhow", + "async-std", + "cookie", + "http", + "infer", + "omnom", + "pin-project-lite", + "serde", + "serde_json", + "url", ] [[package]] name = "httparse" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" [[package]] name = "human-panic" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f357a500abcbd7c5f967c1d45c8838585b36743823b9d43488f24850534e36" dependencies = [ - "backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", - "os_type 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace", + "os_type", + "serde", + "serde_derive", + "termcolor", + "toml", + "uuid", ] [[package]] name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" dependencies = [ - "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "hyper" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "hyper-tls" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] name = "image" version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ed2ada878397b045454ac7cfb011d73132c59f31a955d230bd1f1c2e68eb4a" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", - "jpeg-decoder 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", ] [[package]] name = "image-meta" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2da7b4225d0954c9b8ba1a0dcec85be29f496cba4d85f9390426f810e3ab0d" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "skeptic", + "thiserror", ] [[package]] name = "imap-proto" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f" dependencies = [ - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.1.1", ] [[package]] name = "indexmap" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", +] + +[[package]] +name = "infer" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55c406a76164eb346a829ed4b97b73cb06259078eca01adeb12e8ca308d4123" +dependencies = [ + "byteorder", ] [[package]] name = "inflate" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "adler32", ] [[package]] name = "iovec" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "itertools" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" dependencies = [ - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "either", ] [[package]] name = "itoa" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" [[package]] name = "jpeg-decoder" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", ] [[package]] name = "js-sys" -version = "0.3.36" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" dependencies = [ - "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen", ] [[package]] name = "keccak" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" [[package]] name = "kv-log-macro" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff57d6d215f7ca7eb35a9a64d656ba4d9d2bef114d741dc08048e75e2f5d418" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "log", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ - "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "spin", ] [[package]] @@ -1390,8 +1535,8 @@ name = "lettre" version = "0.9.2" source = "git+https://github.com/deltachat/lettre#12e4f22d8d691fb5d1606e4c9034f5d4d9ba635b" dependencies = [ - "fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "fast_chemail", + "log", ] [[package]] @@ -1399,2066 +1544,1797 @@ name = "lettre_email" version = "0.9.2" source = "git+https://github.com/deltachat/lettre#12e4f22d8d691fb5d1606e4c9034f5d4d9ba635b" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "email 0.0.21 (git+https://github.com/deltachat/rust-email)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lettre 0.9.2 (git+https://github.com/deltachat/lettre)", - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0", + "email", + "lazy_static", + "lettre", + "mime", + "regex", + "time", + "uuid", ] [[package]] name = "lexical-core" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" dependencies = [ - "arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayvec 0.4.12", + "cfg-if", + "rustc_version", + "ryu", + "static_assertions", ] [[package]] name = "libc" -version = "0.2.67" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" [[package]] name = "libm" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libsqlite3-sys" -version = "0.17.1" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d90181c2904c287e5390186be820e5ef311a3c62edebb7d6ca3d6a48ce041d" dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "pkg-config", + "vcpkg", ] [[package]] name = "linked-hash-map" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lock_api" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard", ] [[package]] name = "log" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ - "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map", ] [[package]] name = "lzw" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" [[package]] name = "mailparse" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6435afa64615b3c3efa09cb4066f42dcd236902f30ab48acd36ea59821bb4e0" dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.10.1", + "charset", + "quoted_printable", ] [[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "md-5" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "digest", + "opaque-debug", ] [[package]] name = "memchr" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" dependencies = [ - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", ] [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mime_guess" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" dependencies = [ - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mime", + "unicase", ] [[package]] name = "miniz_oxide" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio-uds" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "adler32", ] [[package]] name = "native-tls" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.10.28 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "net2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] name = "nix" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cc", + "cfg-if", + "libc", + "void", ] [[package]] name = "nodrop" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", + "version_check 0.1.5", ] [[package]] name = "nom" version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" dependencies = [ - "lexical-core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lexical-core", + "memchr", + "version_check 0.9.2", ] [[package]] name = "num-bigint-dig" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d03c330f9f7a2c19e3c0b42698e48141d0809c78cd9b6219f85bd7d7e892aa" dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.7.3", + "serde", + "smallvec", + "zeroize", ] [[package]] name = "num-derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "num-integer" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "num-traits", ] [[package]] name = "num-iter" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "num-integer", + "num-traits", ] [[package]] name = "num-rational" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "num-integer", + "num-traits", ] [[package]] name = "num-traits" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", ] [[package]] name = "num_cpus" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2" + +[[package]] +name = "omnom" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b216cee2e0d6e680f73158d15468c80b39e571c11669cd90556f9a644e9fd3" +dependencies = [ + "memchr", ] [[package]] name = "once_cell" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" [[package]] name = "opaque-debug" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "openssl" -version = "0.10.28" +version = "0.10.29" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.6.1+1.1.1d" +version = "111.9.0+1.1.1g" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dbe10ddd1eb335aba3780eb2eaa13e1b7b441d2562fd962398740927f39ec4" dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", ] [[package]] name = "openssl-sys" -version = "0.9.54" +version = "0.9.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7410fef80af8ac071d4f63755c0ab89ac3df0fd1ea91f1d1f37cf5cec4395990" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-src 111.6.1+1.1.1d (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", ] [[package]] name = "os_type" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edc011af0ae98b7f88cf7e4a83b70a54a75d2b8cb013d6efd02e5956207e9eb" dependencies = [ - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex", ] [[package]] name = "output_vt100" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "packed_simd" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85ea9fc0d4ac0deb6fe7911d38786b32fc11119afd9e9d38b84ff691ce64220" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "parking_lot" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" dependencies = [ - "lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lock_api", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pgp" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8172973101790c866e66966002bf1028d0df27bf6b3b29be86a6fd440d8a4285" dependencies = [ - "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitfield 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", - "block-modes 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "cast5 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfb-mode 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "circular 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crc24 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ed25519-dalek 1.0.0-pre.3 (registry+https://github.com/rust-lang/crates.io-index)", - "flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "num-bigint-dig 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ripemd160 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rsa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "twofish 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "aes", + "base64 0.11.0", + "bitfield", + "block-modes", + "block-padding", + "blowfish", + "buf_redux", + "byteorder", + "cast5", + "cfb-mode", + "chrono", + "circular", + "clear_on_drop", + "crc24", + "derive_builder", + "des", + "digest", + "ed25519-dalek", + "flate2", + "generic-array", + "hex", + "lazy_static", + "log", + "md-5", + "nom 4.2.3", + "num-bigint-dig", + "num-derive", + "num-traits", + "rand 0.7.3", + "ripemd160", + "rsa", + "sha-1", + "sha2", + "sha3", + "smallvec", + "thiserror", + "try_from", + "twofish", + "x25519-dalek", + "zeroize", ] [[package]] name = "pin-project" -version = "0.4.8" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc93aeee735e60ecb40cf740eb319ff23eab1c5748abfdb5c180e4ce49f7791" dependencies = [ - "pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.8" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58db2081ba5b4c93bd6be09c40fd36cb9193a8336c384f3b40012e531aa7e40" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "pin-project-lite" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" [[package]] name = "pin-utils" -version = "0.1.0-alpha.4" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0deb65f46e873ba8aa7c6a8dbe3f23cb1bf59c339a81a1d56361dde4d66ac8" +dependencies = [ + "crossbeam-utils", + "futures-io", + "futures-sink", + "futures-util", +] [[package]] name = "pkg-config" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" [[package]] name = "png" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", - "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "crc32fast", + "deflate", + "inflate", ] [[package]] name = "ppv-lite86" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" [[package]] name = "pretty_assertions" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ctor 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term 0.11.0", + "ctor", + "difference", + "output_vt100", ] [[package]] name = "pretty_env_logger" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" dependencies = [ - "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono", + "env_logger", + "log", ] [[package]] name = "proc-macro-hack" -version = "0.5.11" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" [[package]] name = "proc-macro-nested" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" [[package]] name = "proc-macro2" -version = "1.0.9" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101" dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0", ] [[package]] name = "proptest" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c477819b845fe023d33583ebf10c9f62518c8d79a0960ba5c36d6ac8a55a5b" dependencies = [ - "bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "rusty-fork 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error", + "rand 0.6.5", + "rand_chacha 0.1.1", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", ] [[package]] name = "pulldown-cmark" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", ] [[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] name = "quoted_printable" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" [[package]] name = "r2d2" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "scheduled-thread-pool 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "parking_lot", + "scheduled-thread-pool", ] [[package]] name = "r2d2_sqlite" -version = "0.13.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15ff794e7c8bb8ae20ccac5bac6a93a4a3af708dd801d4094f80da41196f33" dependencies = [ - "r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2", + "rusqlite", ] [[package]] name = "rand" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" dependencies = [ - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", ] [[package]] name = "rand" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "packed_simd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "libc", + "packed_simd", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", ] [[package]] name = "rand_chacha" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7", + "rand_core 0.3.1", ] [[package]] name = "rand_chacha" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", ] [[package]] name = "rand_hc" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1", ] [[package]] name = "rand_isaac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1", ] [[package]] name = "rand_jitter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "rand_core 0.4.2", + "winapi", ] [[package]] name = "rand_os" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", ] [[package]] name = "rand_pcg" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7", + "rand_core 0.4.2", ] [[package]] name = "rand_xorshift" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1", ] [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1", ] [[package]] name = "redox_syscall" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" [[package]] name = "redox_users" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "redox_syscall", + "rust-argon2", ] [[package]] name = "regex" -version = "1.3.4" +version = "1.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" dependencies = [ - "aho-corasick 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.16" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" [[package]] name = "remove_dir_all" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "rental" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f" dependencies = [ - "rental-impl 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rental-impl", + "stable_deref_trait", ] [[package]] name = "rental-impl" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "reqwest" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "ripemd160" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5112e0dbbb87577bfbc56c42450235e3012ce336e29c5befd7807bd626da4a" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "digest", + "opaque-debug", ] [[package]] name = "rsa" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed8692d8e0ea3baae03f0f32ecfc13a6c6f1f85fcd6d9fdefcdf364e70f4df9" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-bigint-dig 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "failure", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "rand 0.7.3", + "subtle", + "zeroize", ] [[package]] name = "rusqlite" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57edf4c4cea4d7e0fab069acb5da9e8e8e5403c78abc81b1f37d83af02148ea5" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "libsqlite3-sys 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "libsqlite3-sys", + "lru-cache", + "memchr", + "time", ] [[package]] name = "rust-argon2" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", ] [[package]] name = "rustc-demangle" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver", ] [[package]] name = "rusty-fork" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" dependencies = [ - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wait-timeout 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv", + "quick-error", + "tempfile", + "wait-timeout", ] [[package]] name = "rustyline" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f47ea1ceb347d2deae482d655dc8eef4bd82363d3329baffa3818bd76fea48b" dependencies = [ - "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "nix 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs", + "libc", + "log", + "memchr", + "nix", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", ] [[package]] name = "ryu" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" [[package]] name = "safemem" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util", ] [[package]] name = "sanitize-filename" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "regex", ] [[package]] name = "schannel" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "winapi", ] [[package]] name = "scheduled-thread-pool" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6" dependencies = [ - "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot", ] +[[package]] +name = "scoped-tls-hkt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63" + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "0.3.4" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" dependencies = [ - "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "0.3.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" dependencies = [ - "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] [[package]] name = "semver" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "semver-parser", + "serde", ] [[package]] name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.104" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" dependencies = [ - "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.104" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "serde_json" -version = "1.0.48" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" dependencies = [ - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa", + "ryu", + "serde", ] [[package]] name = "serde_urlencoded" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dtoa", + "itoa", + "serde", + "url", ] [[package]] name = "sha-1" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", ] [[package]] name = "sha2" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", ] [[package]] name = "sha3" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "byte-tools", + "digest", + "keccak", + "opaque-debug", ] [[package]] name = "skeptic" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165" dependencies = [ - "bytecount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cargo_metadata 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", - "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "serde_json", + "tempdir", + "walkdir", ] [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" [[package]] -name = "snafu" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "smol" +version = "0.1.10" +source = "git+https://github.com/dignifiedquire/smol-1?branch=isolate-nix#41311e709401773c167819d98a56e0c1762d2ba5" dependencies = [ - "doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "snafu-derive 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "async-task", + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "futures-io", + "futures-util", + "libc", + "once_cell", + "piper", + "scoped-tls-hkt", + "slab", + "socket2", + "wepoll-binding", ] [[package]] -name = "snafu-derive" -version = "0.6.2" +name = "socket2" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "redox_syscall", + "winapi", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "stable_deref_trait" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" [[package]] name = "static_assertions" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" [[package]] name = "stop-token" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06855fb7c94d3be9b3a57c4d82dfc8a43bb658fbb3b1dda79de89e748d9eb9dd" dependencies = [ - "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "async-std", + "pin-project-lite", ] [[package]] name = "stream-cipher" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "strsim" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "strum" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6138f8f88a16d90134763314e3fc76fa3ed6a7db4725d6acf9a3ef95a3188d22" [[package]] name = "strum_macros" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0054a7df764039a6cd8592b9de84be4bec368ff081d203a7d5371cbfa8e65c81" dependencies = [ - "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "heck", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "subtle" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" + +[[package]] +name = "surf" +version = "2.0.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d72c302d4a24b2c9d412f1fe5edd892946778d33c423bba087c6c10deeb656ff" +dependencies = [ + "async-std", + "futures 0.3.5", + "http-client", + "http-types", + "log", + "mime", + "mime_guess", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] [[package]] name = "syn" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" dependencies = [ - "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", - "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15", + "synom", + "unicode-xid 0.0.4", ] [[package]] name = "syn" -version = "1.0.16" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "unicode-xid 0.2.0", ] [[package]] name = "synom" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" dependencies = [ - "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4", ] [[package]] name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", + "unicode-xid 0.2.0", ] [[package]] name = "tempdir" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6", + "remove_dir_all", ] [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "rand 0.7.3", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] name = "termcolor" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" dependencies = [ - "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.14" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344" dependencies = [ - "thiserror-impl 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.14" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "thread-local-object" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", ] [[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", ] [[package]] name = "time" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "winapi", ] [[package]] -name = "tokio" -version = "0.2.13" +name = "tokio-io" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-tls" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-util" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12", + "futures 0.1.29", + "log", ] [[package]] name = "toml" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" dependencies = [ - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", ] -[[package]] -name = "tower-service" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "traitobject" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "try-lock" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "try_from" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "twofish" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712d261e83e727c8e2dbb75dacac67c36e35db36a958ee504f2164fc052434e1" dependencies = [ - "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-cipher-trait", + "byteorder", + "opaque-debug", ] [[package]] name = "typenum" -version = "1.11.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" [[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.2", ] [[package]] name = "unicode-bidi" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", ] [[package]] name = "unicode-normalization" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" dependencies = [ - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec", ] [[package]] name = "unicode-segmentation" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" [[package]] name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unsafe-any" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" [[package]] name = "url" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" dependencies = [ - "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "idna", + "matches", + "percent-encoding", ] [[package]] name = "utf8parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" [[package]] name = "uuid" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3", ] [[package]] name = "vcpkg" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" [[package]] name = "version_check" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "walkdir" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ - "same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "same-file", + "winapi", + "winapi-util", ] [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.59" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.59" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94" dependencies = [ - "bumpalo 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.59" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" dependencies = [ - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6", + "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.59" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.59" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad" [[package]] name = "web-sys" -version = "0.3.36" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642" dependencies = [ - "js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "wepoll-binding" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374fff4ff9701ff8b6ad0d14bacd3156c44063632d8c136186ff5967d48999a7" +dependencies = [ + "bitflags", + "wepoll-sys", +] + +[[package]] +name = "wepoll-sys" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9082a777aed991f6769e2b654aa0cb29f1c3d615daf009829b07b66c7aff6a24" +dependencies = [ + "cc", +] [[package]] name = "winapi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winutil" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "x25519-dalek" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" dependencies = [ - "curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", ] [[package]] name = "zeroize" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" dependencies = [ - "zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" dependencies = [ - "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote 1.0.6", + "syn 1.0.23", + "synstructure", ] - -[metadata] -"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" -"checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" -"checksum aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" -"checksum aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" -"checksum aho-corasick 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec" -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -"checksum anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" -"checksum arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -"checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" -"checksum ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" -"checksum async-attributes 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423" -"checksum async-imap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46ff8df29e2a90154d85d3c21e843d1f6d9337dcdcf23b3b5a87228d18122c84" -"checksum async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d40a615e861c981117e15c28c577daf9918cabd2e2d588a5e06811ae79c9da1a" -"checksum async-smtp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3652e5c6072c0694a2bcdb7e8409980d2676bd4f024adf4aab10c68fd2b48f5" -"checksum async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "538ecb01eb64eecd772087e5b6f7540cbc917f047727339a472dafed2185b267" -"checksum async-task 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0ac2c016b079e771204030951c366db398864f5026f84a44dafb0ff20f02085d" -"checksum async-trait 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "750b1c38a1dfadd108da0f01c08f4cdc7ff1bb39b325f9c82cc972361780a6e1" -"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" -"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" -"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" -"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" -"checksum bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80" -"checksum bit-vec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" -"checksum bitfield 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" -"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -"checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" -"checksum block-modes 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "31aa8410095e39fdb732909fb5730a48d5bd7c2e3cd76bd1b07b3dbea130c529" -"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -"checksum blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3" -"checksum broadcaster 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9c972e21e0d055a36cf73e4daae870941fe7a8abcd5ac3396aab9e4c126bd87" -"checksum buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" -"checksum bufstream 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" -"checksum bumpalo 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" -"checksum byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9342e102eac8b1879fbedf9a7e0572c40b0cc5805b663c4d4ca791cae0bae221" -"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -"checksum bytecount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" -"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" -"checksum cargo_metadata 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe" -"checksum cast5 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ce5759b4c52ca74f9a98421817c882f1fd9b0071ae41cd61ab9f9d059c04fd6" -"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" -"checksum cfb-mode 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "190e7b55d3a27cf8879becf61035a141cbc783f3258a41d16d1706719f991345" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" -"checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" -"checksum circular 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" -"checksum clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -"checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" -"checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" -"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" -"checksum crc24 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" -"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -"checksum crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" -"checksum crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -"checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" -"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum ctor 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "47c5e5ac752e18207b12e16b10631ae5f7f68f8805f335f9b817ead83d9ffce1" -"checksum curve25519-dalek 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" -"checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -"checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -"checksum darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -"checksum debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "496b7f8a2f853313c3ca370641d7ff3e42c32974fdccda8f0684599ed0a3ff6b" -"checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" -"checksum derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" -"checksum derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" -"checksum des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af" -"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" -"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" -"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" -"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" -"checksum ed25519-dalek 1.0.0-pre.3 (registry+https://github.com/rust-lang/crates.io-index)" = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2" -"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" -"checksum email 0.0.21 (git+https://github.com/deltachat/rust-email)" = "" -"checksum encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)" = "" -"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -"checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -"checksum encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -"checksum encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -"checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -"checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" -"checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" -"checksum entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" -"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" -"checksum error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" -"checksum escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "39da344028c2227132b2dfa7c186e2104ecc153467583d00ed9c398f9ff693b0" -"checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" -"checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" -"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -"checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -"checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -"checksum fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -"checksum flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" -"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" -"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" -"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" -"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" -"checksum futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" -"checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" -"checksum futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" -"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" -"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" -"checksum futures-timer 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a1de7508b218029b0f01662ed8f61b1c964b3ae99d6f25462d0f55a595109df6" -"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" -"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" -"checksum gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" -"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" -"checksum h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9d5c295d1c0c68e4e42003d75f908f5e16a1edd1cbe0b0d02e4dc2006a384f47" -"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -"checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" -"checksum hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" -"checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" -"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" -"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" -"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -"checksum human-panic 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "39f357a500abcbd7c5f967c1d45c8838585b36743823b9d43488f24850534e36" -"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -"checksum hyper 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e7b15203263d1faa615f9337d79c1d37959439dc46c2b4faab33286fadc2a1c5" -"checksum hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" -"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum image 0.22.5 (registry+https://github.com/rust-lang/crates.io-index)" = "08ed2ada878397b045454ac7cfb011d73132c59f31a955d230bd1f1c2e68eb4a" -"checksum image-meta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a2da7b4225d0954c9b8ba1a0dcec85be29f496cba4d85f9390426f810e3ab0d" -"checksum imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f" -"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" -"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" -"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" -"checksum jpeg-decoder 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0256f0aec7352539102a9efbcb75543227b7ab1117e0f95450023af730128451" -"checksum js-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5" -"checksum keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum kv-log-macro 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c54d9f465d530a752e6ebdc217e081a7a614b48cb200f6f0aee21ba6bc9aabb" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum lettre 0.9.2 (git+https://github.com/deltachat/lettre)" = "" -"checksum lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)" = "" -"checksum lexical-core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" -"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" -"checksum libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" -"checksum libsqlite3-sys 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "266eb8c361198e8d1f682bc974e5d9e2ae90049fb1943890904d11dad7d4a77d" -"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" -"checksum lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" -"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" -"checksum mailparse 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7181507a68fef921f011b0c0f143efca20871da5ab3963bdc064537278469cd2" -"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" -"checksum md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" -"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -"checksum mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -"checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" -"checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" -"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" -"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" -"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" -"checksum nix 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" -"checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -"checksum nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" -"checksum num-bigint-dig 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3d03c330f9f7a2c19e3c0b42698e48141d0809c78cd9b6219f85bd7d7e892aa" -"checksum num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746" -"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -"checksum num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" -"checksum num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3" -"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" -"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" -"checksum once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b" -"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" -"checksum openssl 0.10.28 (registry+https://github.com/rust-lang/crates.io-index)" = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" -"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" -"checksum openssl-src 111.6.1+1.1.1d (registry+https://github.com/rust-lang/crates.io-index)" = "c91b04cb43c1a8a90e934e0cd612e2a5715d976d2d6cff4490278a0cddf35005" -"checksum openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)" = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" -"checksum os_type 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7edc011af0ae98b7f88cf7e4a83b70a54a75d2b8cb013d6efd02e5956207e9eb" -"checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" -"checksum packed_simd 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a85ea9fc0d4ac0deb6fe7911d38786b32fc11119afd9e9d38b84ff691ce64220" -"checksum parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" -"checksum parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" -"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -"checksum pgp 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8172973101790c866e66966002bf1028d0df27bf6b3b29be86a6fd440d8a4285" -"checksum pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" -"checksum pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" -"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" -"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" -"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283" -"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" -"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" -"checksum pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" -"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" -"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" -"checksum proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" -"checksum proptest 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "01c477819b845fe023d33583ebf10c9f62518c8d79a0960ba5c36d6ac8a55a5b" -"checksum pulldown-cmark 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15" -"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" -"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" -"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" -"checksum quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" -"checksum r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" -"checksum r2d2_sqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b061f5b16692bbe81eeb260f92e6fc7d13aea455c4cbe67f5c4aa20aa92d1d9e" -"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" -"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -"checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" -"checksum regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" -"checksum regex-syntax 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1" -"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -"checksum rental 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f" -"checksum rental-impl 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de" -"checksum reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" -"checksum ripemd160 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad5112e0dbbb87577bfbc56c42450235e3012ce336e29c5befd7807bd626da4a" -"checksum rsa 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6ed8692d8e0ea3baae03f0f32ecfc13a6c6f1f85fcd6d9fdefcdf364e70f4df9" -"checksum rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" -"checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" -"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum rusty-fork 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" -"checksum rustyline 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0f47ea1ceb347d2deae482d655dc8eef4bd82363d3329baffa3818bd76fea48b" -"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" -"checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" -"checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -"checksum sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801" -"checksum schannel 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "507a9e6e8ffe0a4e0ebb9a10293e62fdf7657c06f1b8bb07a8fcf697d2abf295" -"checksum scheduled-thread-pool 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f5de7bc31f28f8e6c28df5e1bf3d10610f5fdc14cc95f272853512c70a2bd779" -"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -"checksum security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df" -"checksum security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" -"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" -"checksum serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" -"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" -"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -"checksum sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" -"checksum sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" -"checksum skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165" -"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -"checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" -"checksum snafu 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "546db9181bce2aa22ed883c33d65603b76335b4c2533a98289f54265043de7a1" -"checksum snafu-derive 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bdc75da2e0323f297402fd9c8fdba709bb04e4c627cbe31d19a2c91fc8d9f0e2" -"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" -"checksum static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" -"checksum stop-token 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "06855fb7c94d3be9b3a57c4d82dfc8a43bb658fbb3b1dda79de89e748d9eb9dd" -"checksum stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" -"checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" -"checksum strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6138f8f88a16d90134763314e3fc76fa3ed6a7db4725d6acf9a3ef95a3188d22" -"checksum strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0054a7df764039a6cd8592b9de84be4bec368ff081d203a7d5371cbfa8e65c81" -"checksum subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" -"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -"checksum syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" -"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" -"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" -"checksum thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f0570dc61221295909abdb95c739f2e74325e14293b2026b0a7e195091ec54ae" -"checksum thiserror-impl 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "227362df41d566be41a28f64401e07a043157c21c14b9785a0d8e256f940a8fd" -"checksum thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7da3caa820d0308c84c8654f6cafd81cc3195d45433311cbe22fcf44fc8be071" -"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" -"checksum tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "0fa5e81d6bc4e67fe889d5783bd2a128ab2e0cfa487e0be16b6a8d177b101616" -"checksum tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" -"checksum tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" -"checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" -"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" -"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" -"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -"checksum try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" -"checksum twofish 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d261e83e727c8e2dbb75dacac67c36e35db36a958ee504f2164fc052434e1" -"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" -"checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" -"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" -"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" -"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" -"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" -"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" -"checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" -"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" -"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" -"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" -"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum wait-timeout 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum wasm-bindgen 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d" -"checksum wasm-bindgen-backend 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8" -"checksum wasm-bindgen-futures 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "457414a91863c0ec00090dba537f88ab955d93ca6555862c29b6d860990b8a8a" -"checksum wasm-bindgen-macro 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205" -"checksum wasm-bindgen-macro-support 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d" -"checksum wasm-bindgen-shared 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)" = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8" -"checksum web-sys 0.3.36 (registry+https://github.com/rust-lang/crates.io-index)" = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" -"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" -"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" -"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" diff --git a/Cargo.toml b/Cargo.toml index 683fd39ff..c61569fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" license = "MPL-2.0" [profile.release] -lto = true +# lto = true [dependencies] deltachat_derive = { path = "./deltachat_derive" } @@ -17,15 +17,15 @@ hex = "0.4.0" sha2 = "0.8.0" rand = "0.7.0" smallvec = "1.0.0" -reqwest = { version = "0.10.0", features = ["blocking", "json"] } +surf = { version = "2.0.0-alpha.2", default-features = false, features = ["h1-client"] } num-derive = "0.3.0" num-traits = "0.2.6" -async-smtp = "0.2" +async-smtp = { version = "0.3" } email = { git = "https://github.com/deltachat/rust-email", branch = "master" } lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } -async-imap = "0.2" -async-native-tls = "0.3.1" -async-std = { version = "1.4", features = ["unstable"] } +async-imap = "0.3.1" +async-native-tls = { version = "0.3.3" } +async-std = { version = "1.6.0", features = ["unstable"] } base64 = "0.11" charset = "0.1" percent-encoding = "2.0" @@ -35,12 +35,11 @@ chrono = "0.4.6" indexmap = "1.3.0" lazy_static = "1.4.0" regex = "1.1.6" -rusqlite = { version = "0.21", features = ["bundled"] } -r2d2_sqlite = "0.13.0" +rusqlite = { version = "0.22", features = ["bundled"] } +r2d2_sqlite = "0.15.0" r2d2 = "0.8.5" strum = "0.16.0" strum_macros = "0.16.0" -thread-local-object = "0.1.0" backtrace = "0.3.33" byteorder = "1.3.1" itertools = "0.8.0" @@ -55,17 +54,25 @@ mailparse = "0.12.0" encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" } native-tls = "0.2.3" image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] } -pretty_env_logger = "0.3.1" - -rustyline = { version = "4.1.0", optional = true } +futures = "0.3.4" thiserror = "1.0.14" anyhow = "1.0.28" +async-trait = "0.1.31" +url = "2.1.1" + +pretty_env_logger = { version = "0.3.1", optional = true } +log = {version = "0.4.8", optional = true } +rustyline = { version = "4.1.0", optional = true } +ansi_term = { version = "0.12.1", optional = true } + [dev-dependencies] tempfile = "3.0" pretty_assertions = "0.6.1" pretty_env_logger = "0.3.0" proptest = "0.9.4" +async-std = { version = "1.6.0", features = ["unstable", "attributes"] } +smol = "0.1.10" [workspace] members = [ @@ -76,15 +83,20 @@ members = [ [[example]] name = "simple" path = "examples/simple.rs" +required-features = ["repl"] [[example]] name = "repl" path = "examples/repl/main.rs" -required-features = ["rustyline"] +required-features = ["repl"] [features] -default = ["nightly"] -vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"] +default = [] +internals = [] +repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"] +vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"] nightly = ["pgp/nightly"] +[patch.crates-io] +smol = { git = "https://github.com/dignifiedquire/smol-1", branch = "isolate-nix" } diff --git a/README.md b/README.md index 69a2ef82b..73398338d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment: ``` -curl https://sh.rustup.rs -sSf | sh +$ curl https://sh.rustup.rs -sSf | sh ``` ## Using the CLI client @@ -17,7 +17,7 @@ curl https://sh.rustup.rs -sSf | sh Compile and run Delta Chat Core command line utility, using `cargo`: ``` -cargo run --example repl -- ~/deltachat-db +$ RUST_LOG=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. diff --git a/appveyor.yml b/appveyor.yml index dfb5274aa..b8ea374f3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ environment: install: - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - - rustup-init -yv --default-toolchain nightly-2020-03-12 + - rustup-init -yv --default-toolchain nightly-2020-03-19 - set PATH=%PATH%;%USERPROFILE%\.cargo\bin - rustc -vV - cargo -vV diff --git a/ci_scripts/docker-coredeps/Dockerfile b/ci_scripts/docker-coredeps/Dockerfile index 50f53d1d5..293d9b0a4 100644 --- a/ci_scripts/docker-coredeps/Dockerfile +++ b/ci_scripts/docker-coredeps/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/pypa/manylinux1_x86_64 +FROM quay.io/pypa/manylinux2010_x86_64 # Configure ld.so/ldconfig and pkg-config RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \ diff --git a/ci_scripts/docker-coredeps/deps/build_rust.sh b/ci_scripts/docker-coredeps/deps/build_rust.sh index 239040eb7..72d3e599d 100755 --- a/ci_scripts/docker-coredeps/deps/build_rust.sh +++ b/ci_scripts/docker-coredeps/deps/build_rust.sh @@ -3,9 +3,9 @@ set -e -x # Install Rust -curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-03-12 -y +curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y export PATH=/root/.cargo/bin:$PATH rustc --version # remove some 300-400 MB that we don't need for automated builds -rm -rf /root/.rustup/toolchains/nightly-2020-03-12-x86_64-unknown-linux-gnu/share/ +rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share diff --git a/ci_scripts/remote_python_packaging.sh b/ci_scripts/remote_python_packaging.sh index c6042ace8..61f2e8c9a 100755 --- a/ci_scripts/remote_python_packaging.sh +++ b/ci_scripts/remote_python_packaging.sh @@ -46,6 +46,6 @@ echo "--- Running $CIRCLE_JOB remotely" ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run" mkdir -p workspace -rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/ +rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/ rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/ rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs diff --git a/ci_scripts/run_all.sh b/ci_scripts/run_all.sh index 6565ceedb..5a320d139 100755 --- a/ci_scripts/run_all.sh +++ b/ci_scripts/run_all.sh @@ -21,7 +21,8 @@ export DCC_RS_DEV=$(pwd) export PATH=$PATH:/opt/python/cp35-cp35m/bin export PYTHONDONTWRITEBYTECODE=1 pushd /bin -ln -s /opt/python/cp27-cp27m/bin/python2.7 +rm -f python3.5 +ln -s /opt/python/cp35-cp35m/bin/python3.5 ln -s /opt/python/cp36-cp36m/bin/python3.6 ln -s /opt/python/cp37-cp37m/bin/python3.7 ln -s /opt/python/cp38-cp38/bin/python3.8 diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 46233a86b..8fa08fab8 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -20,10 +20,12 @@ libc = "0.2" human-panic = "1.0.1" num-traits = "0.2.6" serde_json = "1.0" +async-std = "1.6.0" anyhow = "1.0.28" thiserror = "1.0.14" [features] -default = ["vendored", "nightly"] +default = ["vendored"] vendored = ["deltachat/vendored"] nightly = ["deltachat/nightly"] + diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 8c443a943..766cee83c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -19,6 +19,8 @@ typedef struct _dc_msg dc_msg_t; typedef struct _dc_contact dc_contact_t; typedef struct _dc_lot dc_lot_t; typedef struct _dc_provider dc_provider_t; +typedef struct _dc_event dc_event_t; +typedef struct _dc_event_emitter dc_event_emitter_t; /** @@ -30,63 +32,40 @@ typedef struct _dc_provider dc_provider_t; * * Let's start. * - * First of all, you have to **define an event-handler-function** - * that is called by the library on specific events - * (eg. when the configuration is done or when fresh messages arrive). - * With this function you can create a Delta Chat context then: + * First of all, you have to **create a context object** + * bound to a database. + * The database is a normal sqlite-file and is created as needed: * * ~~~ - * #include + * dc_context_t* context = dc_context_new(NULL, "example.db", NULL); + * ~~~ * - * uintptr_t event_handler_func(dc_context_t* context, int event, - * uintptr_t data1, uintptr_t data2) + * After that, make sure, you can **receive events from the context**. + * For that purpose, create an event emitter you can ask for events. + * If there are no event, the emitter will wait until there is one, + * so, in many situations you will do this in a thread: + * + * ~~~ + * void* event_handler(void* context) * { - * return 0; - * } - * - * dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL); - * ~~~ - * - * After that, you should make sure, - * sending and receiving jobs are processed as needed. - * For this purpose, you have to **create two threads:** - * - * ~~~ - * #include - * - * void* imap_thread_func(void* context) - * { - * while (true) { - * dc_perform_imap_jobs(context); - * dc_perform_imap_fetch(context); - * dc_perform_imap_idle(context); + * dc_event_emitter_t* emitter = dc_get_event_emitter(context); + * dc_event_t* event; + * while ((event = dc_get_next_event(emitter)) != NULL) { + * // use the event as needed, eg. dc_event_get_id() returns the type. + * // once you're done, unref the event to avoid memory leakage: + * dc_event_unref(event); * } + * dc_event_emitter_unref(emitter); * } * - * void* smtp_thread_func(void* context) - * { - * while (true) { - * dc_perform_smtp_jobs(context); - * dc_perform_smtp_idle(context); - * } - * } - * - * static pthread_t imap_thread, smtp_thread; - * pthread_create(&imap_thread, NULL, imap_thread_func, context); - * pthread_create(&smtp_thread, NULL, smtp_thread_func, context); + * static pthread_t event_thread; + * pthread_create(&event_thread, NULL, event_handler, context); * ~~~ * * The example above uses "pthreads", * however, you can also use anything else for thread handling. * All deltachat-core-functions, unless stated otherwise, are thread-safe. * - * After that you can **define and open a database.** - * The database is a normal sqlite-file and is created as needed: - * - * ~~~ - * dc_open(context, "example.db", NULL); - * ~~~ - * * Now you can **configure the context:** * * ~~~ @@ -96,15 +75,22 @@ typedef struct _dc_provider dc_provider_t; * dc_configure(context); * ~~~ * - * dc_configure() returns immediately, the configuration itself may take a while - * and is done by a job in the imap-thread you've defined above. + * dc_configure() returns immediately, + * the configuration itself runs in background and may take a while. * Once done, the #DC_EVENT_CONFIGURE_PROGRESS reports success - * to the event_handler_func() that is also defined above. + * to the event_handler() you've defined above. * * The configuration result is saved in the database, * on subsequent starts it is not needed to call dc_configure() * (you can check this using dc_is_configured()). * + * On a successfully configured context, + * you can finally **connect to the servers:** + * + * ~~~ + * dc_start_io(context); + * ~~~ + * * Now you can **send the first message:** * * ~~~ @@ -116,11 +102,11 @@ typedef struct _dc_provider dc_provider_t; * ~~~ * * dc_send_text_msg() returns immediately; - * the sending itself is done by a job in the smtp-thread you've defined above. + * the sending itself is done in the background. * If you check the testing address (bob) * and you should have received a normal email. * Answer this email in any email program with "Got it!" - * and the imap-thread you've create above will **receive the message**. + * and the IO you started above will **receive the message**. * * You can then **list all messages** of a chat as follow: * @@ -164,19 +150,6 @@ typedef struct _dc_provider dc_provider_t; * - The issue-tracker for the core library is here: * * - * The following points are important mainly - * for the authors of the library itself: - * - * - For indentation, use tabs. - * Alignments that are not placed at the beginning of a line - * should be done with spaces. - * - * - For padding between functions, - * classes etc. use 2 empty lines - * - * - Source files are encoded as UTF-8 with Unix line endings - * (a simple `LF`, `0x0A` or `\n`) - * * If you need further assistance, * please do not hesitate to contact us * through the channels shown at https://delta.chat/en/contribute @@ -199,20 +172,6 @@ typedef struct _dc_provider dc_provider_t; * settings. */ - -/** - * Callback function that should be given to dc_context_new(). - * - * @memberof dc_context_t - * @param context The context object as returned by dc_context_new(). - * @param event one of the @ref DC_EVENT constants - * @param data1 depends on the event parameter - * @param data2 depends on the event parameter - * @return events do not expect a return value, just always return 0 - */ -typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t data1, uintptr_t data2); - - // create/open/config/information /** @@ -220,18 +179,6 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t * opened, connected and mails are fetched. * * @memberof dc_context_t - * @param cb a callback function that is called for events (update, - * state changes etc.) and to get some information from the client (eg. translation - * for a given string). - * See @ref DC_EVENT for a list of possible events that may be passed to the callback. - * - The callback MAY be called from _any_ thread, not only the main/GUI thread! - * - The callback MUST NOT call any dc_* and related functions unless stated - * otherwise! - * - The callback SHOULD return _fast_, for GUI updates etc. you should - * post yourself an asynchronous message to your GUI thread, if needed. - * - events do not expect a return value, just always return 0. - * @param userdata can be used by the client for any purpuse. He finds it - * later in dc_get_userdata(). * @param os_name is only for decorative use * and is shown eg. in the `X-Mailer:` header * in the form "Delta Chat Core /". @@ -239,11 +186,16 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t * the used environment and/or the version here. * It is okay to give NULL, in this case `X-Mailer:` header * is set to "Delta Chat Core ". + * @param dbfile The file to use to store the database, + * something like `~/file` won't work, use absolute paths. + * @param blobdir A directory to store the blobs in; a trailing slash is not needed. + * If you pass NULL or the empty string, deltachat-core creates a directory + * beside _dbfile_ with the same name and the suffix `-blobs`. * @return A context object with some public members. * The object must be passed to the other context functions * and must be freed using dc_context_unref() after usage. */ -dc_context_t* dc_context_new (dc_callback_t cb, void* userdata, const char* os_name); +dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir); /** @@ -262,56 +214,21 @@ void dc_context_unref (dc_context_t* context); /** - * Get user data associated with a context object. + * Create the event emitter that is used to receive events. + * The library will emit various @ref DC_EVENT events as "new message", "message read" etc. + * To get these events, you have to create an event emitter using this function + * and call dc_get_next_event() on the emitter. * * @memberof dc_context_t * @param context The context object as created by dc_context_new(). - * @return User data, this is the second parameter given to dc_context_new(). - */ -void* dc_get_userdata (dc_context_t* context); - - -/** - * Open context database. If the given file does not exist, it is - * created and can be set up using dc_set_config() afterwards. + * @return Returns the event emitter, NULL on errors. + * Must be freed using dc_event_emitter_unref() after usage. * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param dbfile The file to use to store the database, something like `~/file` won't - * work on all systems, if in doubt, use absolute paths. - * @param blobdir A directory to store the blobs in; a trailing slash is not needed. - * If you pass NULL or the empty string, deltachat-core creates a directory - * beside _dbfile_ with the same name and the suffix `-blobs`. - * @return 1 on success, 0 on failure - * eg. if the file is not writable - * or if there is already a database opened for the context. + * Note: Use only one event emitter per context. + * Having more than one event emitter running at the same time on the same context + * will result in events randomly delivered to the one or to the other. */ -int dc_open (dc_context_t* context, const char* dbfile, const char* blobdir); - - -/** - * Close context database opened by dc_open(). - * Before this, connections to SMTP and IMAP are closed; these connections - * are started automatically as needed eg. by sending for fetching messages. - * This function is also implicitly called by dc_context_unref(). - * Multiple calls to this functions are okay, the function takes care not - * to free objects twice. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @return None. - */ -void dc_close (dc_context_t* context); - - -/** - * Check if the context database is open. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @return 0=context is not open, 1=context is open. - */ -int dc_is_open (const dc_context_t* context); +dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context); /** @@ -513,9 +430,7 @@ char* dc_get_oauth2_url (dc_context_t* context, const char* /** * Configure a context. - * For this purpose, the function creates a job - * that is executed in the IMAP-thread then; - * this requires to call dc_perform_imap_jobs() regularly. + * While configuration IO must not be started, if needed stop IO using dc_stop_io() first. * If the context is already configured, * this function will try to change the configuration. * @@ -577,311 +492,40 @@ void dc_configure (dc_context_t* context); * @return 1=context is configured and can be used; * 0=context is not configured and a configuration by dc_configure() is required. */ -int dc_is_configured (const dc_context_t* context); +int dc_is_configured (const dc_context_t* context); /** - * Execute pending imap-jobs. - * This function and dc_perform_imap_fetch() and dc_perform_imap_idle() - * must be called from the same thread, typically in a loop. - * - * Example: - * - * void* imap_thread_func(void* context) - * { - * while (true) { - * dc_perform_imap_jobs(context); - * dc_perform_imap_fetch(context); - * dc_perform_imap_idle(context); - * } - * } - * - * // start imap-thread that runs forever - * pthread_t imap_thread; - * pthread_create(&imap_thread, NULL, imap_thread_func, context); - * - * ... program runs ... - * - * // network becomes available again - - * // the interrupt causes dc_perform_imap_idle() in the thread above - * // to return so that jobs are executed and messages are fetched. - * dc_maybe_network(context); + * Start job and IMAP/SMTP tasks. + * If IO is already running, nothing happens. + * To check the current IO state, use dc_is_io_running(). * * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. + * @param context The context object as created by dc_context_new(). + * @return None */ -void dc_perform_imap_jobs (dc_context_t* context); - +void dc_start_io (dc_context_t* context); /** - * Fetch new messages, if any. - * This function and dc_perform_imap_jobs() and dc_perform_imap_idle() must be called from the same thread, - * typically in a loop. - * - * See dc_perform_imap_jobs() for an example. + * Check if IO (SMTP/IMAP/Jobs) has been started. * * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. + * @param context The context object as created by dc_context_new(). + * @return 1=IO is running; + * 0=IO is not running. */ -void dc_perform_imap_fetch (dc_context_t* context); - +int dc_is_io_running(const dc_context_t* context); /** - * Wait for messages or jobs. - * This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread, - * typically in a loop. - * - * You should call this function directly after calling dc_perform_imap_fetch(). - * - * See dc_perform_imap_jobs() for an example. + * Stop job and IMAP/SMTP tasks and return when they are finished. + * If IO is not running, nothing happens. + * To check the current IO state, use dc_is_io_running(). * * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. + * @param context The context object as created by dc_context_new(). + * @return None */ -void dc_perform_imap_idle (dc_context_t* context); - - -/** - * Interrupt waiting for imap-jobs. - * If dc_perform_imap_jobs(), dc_perform_imap_fetch() and dc_perform_imap_idle() are called in a loop, - * calling this function causes imap-jobs to be executed and messages to be fetched. - * - * dc_interrupt_imap_idle() does _not_ interrupt dc_perform_imap_jobs() or dc_perform_imap_fetch(). - * If the imap-thread is inside one of these functions when dc_interrupt_imap_idle() is called, however, - * the next call of the imap-thread to dc_perform_imap_idle() is interrupted immediately. - * - * Internally, this function is called whenever a imap-jobs should be processed - * (delete message, markseen etc.). - * - * When you need to call this function just because to get jobs done after network changes, - * use dc_maybe_network() instead. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_interrupt_imap_idle (dc_context_t* context); - - -/** - * Execute pending mvbox-jobs. - * This function and dc_perform_mvbox_fetch() and dc_perform_mvbox_idle() - * must be called from the same thread, typically in a loop. - * - * Example: - * - * void* mvbox_thread_func(void* context) - * { - * while (true) { - * dc_perform_mvbox_jobs(context); - * dc_perform_mvbox_fetch(context); - * dc_perform_mvbox_idle(context); - * } - * } - * - * // start mvbox-thread that runs forever - * pthread_t mvbox_thread; - * pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context); - * - * ... program runs ... - * - * // network becomes available again - - * // the interrupt causes dc_perform_mvbox_idle() in the thread above - * // to return so that jobs are executed and messages are fetched. - * dc_maybe_network(context); - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_mvbox_jobs (dc_context_t* context); - - -/** - * Fetch new messages from the MVBOX, if any. - * The MVBOX is a folder on the account where chat messages are moved to. - * The moving is done to not disturb shared accounts that are used by both, - * Delta Chat and a classical MUA. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_mvbox_fetch (dc_context_t* context); - - -/** - * Wait for messages or jobs in the MVBOX-thread. - * This function and dc_perform_mvbox_fetch(). - * must be called from the same thread, typically in a loop. - * - * You should call this function directly after calling dc_perform_mvbox_fetch(). - * - * See dc_perform_mvbox_fetch() for an example. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_mvbox_idle (dc_context_t* context); - - -/** - * Interrupt waiting for MVBOX-fetch. - * dc_interrupt_mvbox_idle() does _not_ interrupt dc_perform_mvbox_fetch(). - * If the MVBOX-thread is inside this function when dc_interrupt_mvbox_idle() is called, however, - * the next call of the MVBOX-thread to dc_perform_mvbox_idle() is interrupted immediately. - * - * Internally, this function is called whenever a imap-jobs should be processed. - * - * When you need to call this function just because to get jobs done after network changes, - * use dc_maybe_network() instead. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_interrupt_mvbox_idle (dc_context_t* context); - - -/** - * Execute pending sentbox-jobs. - * This function and dc_perform_sentbox_fetch() and dc_perform_sentbox_idle() - * must be called from the same thread, typically in a loop. - * - * Example: - * - * void* sentbox_thread_func(void* context) - * { - * while (true) { - * dc_perform_sentbox_jobs(context); - * dc_perform_sentbox_fetch(context); - * dc_perform_sentbox_idle(context); - * } - * } - * - * // start sentbox-thread that runs forever - * pthread_t sentbox_thread; - * pthread_create(&sentbox_thread, NULL, sentbox_thread_func, context); - * - * ... program runs ... - * - * // network becomes available again - - * // the interrupt causes dc_perform_sentbox_idle() in the thread above - * // to return so that jobs are executed and messages are fetched. - * dc_maybe_network(context); - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_sentbox_jobs (dc_context_t* context); - - -/** - * Fetch new messages from the Sent folder, if any. - * This function and dc_perform_sentbox_idle() - * must be called from the same thread, typically in a loop. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_sentbox_fetch (dc_context_t* context); - - -/** - * Wait for messages or jobs in the SENTBOX-thread. - * This function and dc_perform_sentbox_fetch() - * must be called from the same thread, typically in a loop. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_sentbox_idle (dc_context_t* context); - - -/** - * Interrupt waiting for messages or jobs in the SENTBOX-thread. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_interrupt_sentbox_idle (dc_context_t* context); - - -/** - * Execute pending smtp-jobs. - * This function and dc_perform_smtp_idle() must be called from the same thread, - * typically in a loop. - * - * Example: - * - * void* smtp_thread_func(void* context) - * { - * while (true) { - * dc_perform_smtp_jobs(context); - * dc_perform_smtp_idle(context); - * } - * } - * - * // start smtp-thread that runs forever - * pthread_t smtp_thread; - * pthread_create(&smtp_thread, NULL, smtp_thread_func, context); - * - * ... program runs ... - * - * // network becomes available again - - * // the interrupt causes dc_perform_smtp_idle() in the thread above - * // to return so that jobs are executed - * dc_maybe_network(context); - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_smtp_jobs (dc_context_t* context); - - -/** - * Wait for smtp-jobs. - * This function and dc_perform_smtp_jobs() must be called from the same thread, - * typically in a loop. - * - * See dc_interrupt_smtp_idle() for an example. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_perform_smtp_idle (dc_context_t* context); - - -/** - * Interrupt waiting for smtp-jobs. - * If dc_perform_smtp_jobs() and dc_perform_smtp_idle() are called in a loop, - * calling this function causes jobs to be executed. - * - * dc_interrupt_smtp_idle() does _not_ interrupt dc_perform_smtp_jobs(). - * If the smtp-thread is inside this function when dc_interrupt_smtp_idle() is called, however, - * the next call of the smtp-thread to dc_perform_smtp_idle() is interrupted immediately. - * - * Internally, this function is called whenever a message is to be sent. - * - * When you need to call this function just because to get jobs done after network changes, - * use dc_maybe_network() instead. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @return None. - */ -void dc_interrupt_smtp_idle (dc_context_t* context); - +void dc_stop_io(dc_context_t* context); /** * This function can be called whenever there is a hint @@ -895,6 +539,7 @@ void dc_interrupt_smtp_idle (dc_context_t* context); void dc_maybe_network (dc_context_t* context); + /** * Save a keypair as the default keys for the user. * @@ -1121,6 +766,23 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch */ uint32_t dc_send_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg); +/** + * Send a message defined by a dc_msg_t object to a chat, synchronously. + * This bypasses the IO scheduler and creates its own SMTP connection. Which means + * this is useful when the scheduler is not running. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the message to. + * If dc_prepare_msg() was called before, this parameter can be 0. + * @param msg Message object to send to the chat defined by the chat ID. + * On succcess, msg_id of the object is set up, + * The function does not take ownership of the object, + * so you have to free it using dc_msg_unref() as usual. + * @return The ID of the message that is about to be sent. 0 in case of errors. + */ +uint32_t dc_send_msg_sync (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg); + /** * Send a simple text message a given chat. @@ -1976,9 +1638,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co /** * Import/export things. - * For this purpose, the function creates a job that is executed in the IMAP-thread then; - * this requires to call dc_perform_imap_jobs() regularly. - * * What to do is defined by the _what_ parameter which may be one of the following: * * - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`. @@ -2751,7 +2410,6 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist); * last-message-date: * avatar-path: path-to-blobfile * is_verified: yes/no - * @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned. */ char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id); @@ -4160,11 +3818,122 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); #define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages +/** + * @class dc_event_emitter_t + * + * Opaque object that is used to get events. + * You can get an event emitter from a context using dc_get_event_emitter(). + */ + +/** + * Get the next event from an event emitter object. + * + * @memberof dc_event_emitter_t + * @param emitter Event emitter object as returned from dc_get_event_emitter(). + * @return An event as an dc_event_t object. + * You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on; + * if you are done with the event, you have to free the event using dc_event_unref(). + * If NULL is returned, the context belonging to the event emitter is unref'd and the no more events will come; + * in this case, free the event emitter using dc_event_emitter_unref(). + */ +dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter); + + +/** + * Free an event emitter object. + * + * @memberof dc_event_emitter_t + * @param emitter Event emitter object as returned from dc_get_event_emitter(). + * If NULL is given, nothing is done and an error is logged. + * @return None. + */ +void dc_event_emitter_unref(dc_event_emitter_t* emitter); + + +/** + * @class dc_event_t + * + * Opaque object describing a single event. + * To get events, call dc_get_next_event() on an event emitter created by dc_get_event_emitter(). + */ + +/** + * Get the event-id from an event object. + * The event-id is one of the @ref DC_EVENT constants. + * There may be additional data belonging to an event, + * to get them, use dc_event_get_data1_int(), dc_event_get_data2_int() and dc_event_get_data2_str(). + * + * @memberof dc_event_t + * @param event Event object as returned from dc_get_next_event(). + * @return once of the @ref DC_EVENT constants. + * 0 on errors. + */ +int dc_event_get_id(dc_event_t* event); + + +/** + * Get a data associated with an event object. + * The meaning of the data depends on the event-id + * returned as @ref DC_EVENT constants by dc_event_get_id(). + * See also dc_event_get_data2_int() and dc_event_get_data2_str(). + * + * @memberof dc_event_t + * @param event Event object as returned from dc_get_next_event(). + * @return "data1" as a signed integer, at least 32bit, + * the meaning depends on the event type associated with this event. + */ +int dc_event_get_data1_int(dc_event_t* event); + + +/** + * Get a data associated with an event object. + * The meaning of the data depends on the event-id + * returned as @ref DC_EVENT constants by dc_event_get_id(). + * See also dc_event_get_data2_int() and dc_event_get_data2_str(). + * + * @memberof dc_event_t + * @param event Event object as returned from dc_get_next_event(). + * @return "data2" as a signed integer, at least 32bit, + * the meaning depends on the event type associated with this event. + */ +int dc_event_get_data2_int(dc_event_t* event); + + +/** + * Get a data associated with an event object. + * The meaning of the data depends on the event-id + * returned as @ref DC_EVENT constants by dc_event_get_id(). + * See also dc_event_get_data1_int() and dc_event_get_data2_int(). + * + * @memberof dc_event_t + * @param event Event object as returned from dc_get_next_event(). + * @return "data2" as a string, + * the meaning depends on the event type associated with this event. + * Once you're done with the string, you have to unref it using dc_unref_str(). + */ +char* dc_event_get_data2_str(dc_event_t* event); + + +/** + * Free memory used by an event object. + * If you forget to do this for an event, this will result in memory leakage. + * + * @memberof dc_event_t + * @param event Event object as returned from dc_get_next_event(). + * @return None. + */ +void dc_event_unref(dc_event_t* event); + + /** * @defgroup DC_EVENT DC_EVENT * - * These constants are used as events - * reported to the callback given to dc_context_new(). + * These constants are used as event-id + * in events returned by dc_get_next_event(). + * + * Events typically come with some additional data, + * use dc_event_get_data1_int(), dc_event_get_data2_int() and dc_event_get_data2_str() to read this data. + * The meaning of the data depends on the event. * * @addtogroup DC_EVENT * @{ @@ -4172,13 +3941,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * The library-user may write an informational string to the log. - * Passed to the callback given to dc_context_new(). * * This event should not be reported to the end-user using a popup or something like that. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_INFO 100 @@ -4187,8 +3954,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when SMTP connection is established and login was successful. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_SMTP_CONNECTED 101 @@ -4197,8 +3963,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when IMAP connection is established and login was successful. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_IMAP_CONNECTED 102 @@ -4206,8 +3971,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when a message was successfully sent to the SMTP server. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_SMTP_MESSAGE_SENT 103 @@ -4215,8 +3979,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when a message was successfully marked as deleted on the IMAP server. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_IMAP_MESSAGE_DELETED 104 @@ -4224,8 +3987,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when a message was successfully moved on IMAP. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_IMAP_MESSAGE_MOVED 105 @@ -4233,8 +3995,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when an IMAP folder was emptied. * * @param data1 0 - * @param data2 (const char*) folder name. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Folder name. */ #define DC_EVENT_IMAP_FOLDER_EMPTIED 106 @@ -4242,8 +4003,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when a new blob file was successfully written * * @param data1 0 - * @param data2 (const char*) path name - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Path name */ #define DC_EVENT_NEW_BLOB_FILE 150 @@ -4251,27 +4011,23 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * Emitted when a blob file was successfully deleted * * @param data1 0 - * @param data2 (const char*) path name - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Path name */ #define DC_EVENT_DELETED_BLOB_FILE 151 /** * The library-user should write a warning string to the log. - * Passed to the callback given to dc_context_new(). * * This event should not be reported to the end-user using a popup or something like that. * * @param data1 0 - * @param data2 (const char*) Warning string in english language. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Warning string in english language. */ #define DC_EVENT_WARNING 300 /** * The library-user should report an error to the end-user. - * Passed to the callback given to dc_context_new(). * * As most things are asynchronous, things may go wrong at any time and the user * should not be disturbed by a dialog or so. Instead, use a bubble or so. @@ -4283,10 +4039,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * in a messasge box then. * * @param data1 0 - * @param data2 (const char*) Error string, always set, never NULL. + * @param data2 (char*) Error string, always set, never NULL. * Some error strings are taken from dc_set_stock_translation(), * however, most error strings will be in english language. - * Must not be unref'd or modified and is valid only until the callback returns. */ #define DC_EVENT_ERROR 400 @@ -4308,8 +4063,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * * @param data1 (int) 1=first/new network error, should be reported the user; * 0=subsequent network error, should be logged only - * @param data2 (const char*) Error string, always set, never NULL. - * Must not be unref'd or modified and is valid only until the callback returns. + * @param data2 (char*) Error string, always set, never NULL. */ #define DC_EVENT_ERROR_NETWORK 401 @@ -4322,9 +4076,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * dc_send_text_msg() or another sending function. * * @param data1 0 - * @param data2 (const char*) Info string in english language. - * Must not be unref'd or modified - * and is valid only until the callback returns. + * @param data2 (char*) Info string in english language. */ #define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410 @@ -4442,9 +4194,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * A typical purpose for a handler of this event may be to make the file public to some system * services. * - * @param data1 (const char*) Path and file name. - * Must not be unref'd or modified and is valid only until the callback returns. - * @param data2 0 + * @param data1 0 + * @param data2 (char*) Path and file name. */ #define DC_EVENT_IMEX_FILE_WRITTEN 2052 @@ -4491,8 +4242,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); #define DC_ERROR_SEE_STRING 0 // not used anymore #define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore #define DC_STR_SELFNOTINGRP 21 // not used anymore -#define DC_EVENT_DATA1_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || (e)==DC_EVENT_FILE_COPIED) -#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499) +#define DC_EVENT_DATA1_IS_STRING(e) 0 // not used anymore +#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499)) #define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore #define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore #define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 1e75bd359..f3fe11c37 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -15,14 +15,12 @@ extern crate serde_json; use std::collections::BTreeMap; use std::convert::TryInto; -use std::ffi::CString; use std::fmt::Write; use std::ptr; use std::str::FromStr; -use std::sync::RwLock; use std::time::{Duration, SystemTime}; -use libc::uintptr_t; +use async_std::task::{block_on, spawn}; use num_traits::{FromPrimitive, ToPrimitive}; use deltachat::chat::{ChatId, ChatVisibility, MuteDuration}; @@ -51,176 +49,44 @@ use self::string::*; // dc_context_t -/// The FFI context struct. -/// -/// This structure represents the [Context] on the FFI interface. -/// Since it is returned by [dc_context_new] before it is initialised -/// by [dc_open] it needs to store the actual [Context] in an [Option] -/// and protected by an [RwLock]. Other than that it needs to store -/// the data which is passed into [dc_context_new]. -pub struct ContextWrapper { - cb: Option, - userdata: *mut libc::c_void, - os_name: String, - inner: RwLock>, -} - -unsafe impl Send for ContextWrapper {} -unsafe impl Sync for ContextWrapper {} - -/// Callback function that should be given to [dc_context_new]. -/// -/// @memberof [dc_context_t] -/// @param context The context object as returned by [dc_context_new]. -/// @param event one of the @ref DC_EVENT constants -/// @param data1 depends on the event parameter -/// @param data2 depends on the event parameter -/// @return return 0 unless stated otherwise in the event parameter documentation -pub type dc_callback_t = - unsafe extern "C" fn(_: &dc_context_t, _: i32, _: uintptr_t, _: uintptr_t) -> uintptr_t; - /// Struct representing the deltachat context. -/// -/// See [ContextWrapper] for implementation details. -pub type dc_context_t = ContextWrapper; - -impl ContextWrapper { - /// Log a warning on the FFI context. - /// - /// Like [error] but logs as a warning which only goes to the - /// logfile rather than being shown directly to the user. - unsafe fn warning(&self, msg: &str) { - self.translate_cb(Event::Warning(msg.to_string())); - } - - /// Unlock the context and execute a closure with it. - /// - /// This unlocks the context and gets a read lock. The Rust - /// [Context] object it passed as only argument to the closure - /// which can now do Rust API calls using it. The return value of - /// the closure will be returned by this function. When the - /// closure returns the read lock is released. - /// - /// If the context is not open an error is logged via the callback - /// and `Err(())` is returned. - /// - /// This function returns a [Result] allowing the caller to supply - /// the appropriate return value for an error return since this - /// differs for various functions on the FFI API: sometimes 0, - /// NULL, an empty string etc. - unsafe fn with_inner(&self, ctxfn: F) -> Result - where - F: FnOnce(&Context) -> T, - { - self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| { - self.warning(&err.to_string()); - }) - } - - /// Unlock the context and execute a closure with it. - /// - /// This is like [ContextWrapper::with_inner] but uses - /// [anyhow::Error] as error type. This allows you to write a - /// closure which could produce many errors, use the `?` operator - /// to return them and handle them all as the return of this call. - fn try_inner(&self, ctxfn: F) -> Result - where - F: FnOnce(&Context) -> Result, - { - let guard = self.inner.read().unwrap(); - match guard.as_ref() { - Some(ref ctx) => ctxfn(ctx), - None => Err(anyhow::format_err!("context not open")), - } - } - - /// Translates the callback from the rust style to the C-style version. - unsafe fn translate_cb(&self, event: Event) { - if let Some(ffi_cb) = self.cb { - let event_id = event.as_id(); - match event { - Event::Info(msg) - | Event::SmtpConnected(msg) - | Event::ImapConnected(msg) - | Event::SmtpMessageSent(msg) - | Event::ImapMessageDeleted(msg) - | Event::ImapMessageMoved(msg) - | Event::ImapFolderEmptied(msg) - | Event::NewBlobFile(msg) - | Event::DeletedBlobFile(msg) - | Event::Warning(msg) - | Event::Error(msg) - | Event::ErrorNetwork(msg) - | Event::ErrorSelfNotInGroup(msg) => { - let data2 = CString::new(msg).unwrap_or_default(); - ffi_cb(self, event_id, 0, data2.as_ptr() as uintptr_t); - } - Event::MsgsChanged { chat_id, msg_id } - | Event::IncomingMsg { chat_id, msg_id } - | Event::MsgDelivered { chat_id, msg_id } - | Event::MsgFailed { chat_id, msg_id } - | Event::MsgRead { chat_id, msg_id } => { - ffi_cb( - self, - event_id, - chat_id.to_u32() as uintptr_t, - msg_id.to_u32() as uintptr_t, - ); - } - Event::ChatModified(chat_id) => { - ffi_cb(self, event_id, chat_id.to_u32() as uintptr_t, 0); - } - Event::ContactsChanged(id) | Event::LocationChanged(id) => { - let id = id.unwrap_or_default(); - ffi_cb(self, event_id, id as uintptr_t, 0); - } - Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => { - ffi_cb(self, event_id, progress as uintptr_t, 0); - } - Event::ImexFileWritten(file) => { - let data1 = file.to_c_string().unwrap_or_default(); - ffi_cb(self, event_id, data1.as_ptr() as uintptr_t, 0); - } - Event::SecurejoinInviterProgress { - contact_id, - progress, - } - | Event::SecurejoinJoinerProgress { - contact_id, - progress, - } => { - ffi_cb( - self, - event_id, - contact_id as uintptr_t, - progress as uintptr_t, - ); - } - } - } - } -} +pub type dc_context_t = Context; #[no_mangle] pub unsafe extern "C" fn dc_context_new( - cb: Option, - userdata: *mut libc::c_void, os_name: *const libc::c_char, + dbfile: *const libc::c_char, + blobdir: *const libc::c_char, ) -> *mut dc_context_t { setup_panic!(); + if dbfile.is_null() { + eprintln!("ignoring careless call to dc_context_new()"); + return ptr::null_mut(); + } + let os_name = if os_name.is_null() { String::from("DcFFI") } else { to_string_lossy(os_name) }; - let ffi_ctx = ContextWrapper { - cb, - userdata, - os_name, - inner: RwLock::new(None), + + let ctx = if blobdir.is_null() || *blobdir == 0 { + block_on(Context::new(os_name, as_path(dbfile).to_path_buf().into())) + } else { + block_on(Context::with_blobdir( + os_name, + as_path(dbfile).to_path_buf().into(), + as_path(blobdir).to_path_buf().into(), + )) }; - Box::into_raw(Box::new(ffi_ctx)) + match ctx { + Ok(ctx) => Box::into_raw(Box::new(ctx)), + Err(err) => { + eprintln!("failed to create context: {}", err); + ptr::null_mut() + } + } } /// Release the context structure. @@ -232,79 +98,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_context_unref()"); return; } - let ffi_context = &mut *context; - Box::from_raw(ffi_context); -} - -#[no_mangle] -pub unsafe extern "C" fn dc_get_userdata(context: *mut dc_context_t) -> *mut libc::c_void { - if context.is_null() { - eprintln!("ignoring careless call to dc_get_userdata()"); - return ptr::null_mut(); - } - let ffi_context = &mut *context; - ffi_context.userdata -} - -#[no_mangle] -pub unsafe extern "C" fn dc_open( - context: *mut dc_context_t, - dbfile: *const libc::c_char, - blobdir: *const libc::c_char, -) -> libc::c_int { - if context.is_null() || dbfile.is_null() { - eprintln!("ignoring careless call to dc_open()"); - return 0; - } - let ffi_context = &*context; - let rust_cb = move |_ctx: &Context, evt: Event| ffi_context.translate_cb(evt); - - let ctx = if blobdir.is_null() || *blobdir == 0 { - Context::new( - Box::new(rust_cb), - ffi_context.os_name.clone(), - as_path(dbfile).to_path_buf(), - ) - } else { - Context::with_blobdir( - Box::new(rust_cb), - ffi_context.os_name.clone(), - as_path(dbfile).to_path_buf(), - as_path(blobdir).to_path_buf(), - ) - }; - match ctx { - Ok(ctx) => { - let mut inner_guard = ffi_context.inner.write().unwrap(); - *inner_guard = Some(ctx); - 1 - } - Err(_) => 0, - } -} - -#[no_mangle] -pub unsafe extern "C" fn dc_close(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_close()"); - return; - } - let ffi_context = &mut *context; - ffi_context.inner.write().unwrap().take(); -} - -#[no_mangle] -pub unsafe extern "C" fn dc_is_open(context: *mut dc_context_t) -> libc::c_int { - if context.is_null() { - eprintln!("ignoring careless call to dc_is_open()"); - return 0; - } - let ffi_context = &*context; - let inner_guard = ffi_context.inner.read().unwrap(); - match *inner_guard { - Some(_) => 1, - None => 0, - } + Box::from_raw(context); } #[no_mangle] @@ -313,10 +107,8 @@ pub unsafe extern "C" fn dc_get_blobdir(context: *mut dc_context_t) -> *mut libc eprintln!("ignoring careless call to dc_get_blobdir()"); return "".strdup(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| ctx.get_blobdir().to_string_lossy().strdup()) - .unwrap_or_else(|_| "".strdup()) + let ctx = &*context; + ctx.get_blobdir().to_string_lossy().strdup() } #[no_mangle] @@ -329,18 +121,17 @@ pub unsafe extern "C" fn dc_set_config( eprintln!("ignoring careless call to dc_set_config()"); return 0; } - let ffi_context = &*context; + let ctx = &*context; match config::Config::from_str(&to_string_lossy(key)) { // When ctx.set_config() fails it already logged the error. // TODO: Context::set_config() should not log this - Ok(key) => ffi_context - .with_inner(|ctx| { - ctx.set_config(key, to_opt_string_lossy(value).as_ref().map(|x| x.as_str())) - .is_ok() as libc::c_int - }) - .unwrap_or(0), + Ok(key) => block_on(async move { + ctx.set_config(key, to_opt_string_lossy(value).as_ref().map(|x| x.as_str())) + .await + .is_ok() as libc::c_int + }), Err(_) => { - ffi_context.warning("dc_set_config(): invalid key"); + warn!(ctx, "dc_set_config(): invalid key"); 0 } } @@ -355,13 +146,11 @@ pub unsafe extern "C" fn dc_get_config( eprintln!("ignoring careless call to dc_get_config()"); return "".strdup(); } - let ffi_context = &*context; + let ctx = &*context; match config::Config::from_str(&to_string_lossy(key)) { - Ok(key) => ffi_context - .with_inner(|ctx| ctx.get_config(key).unwrap_or_default().strdup()) - .unwrap_or_else(|_| "".strdup()), + Ok(key) => block_on(async move { ctx.get_config(key).await.unwrap_or_default().strdup() }), Err(_) => { - ffi_context.warning("dc_get_config(): invalid key"); + warn!(ctx, "dc_get_config(): invalid key"); "".strdup() } } @@ -378,10 +167,11 @@ pub unsafe extern "C" fn dc_set_stock_translation( return 0; } let msg = to_string_lossy(stock_msg); - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match StockMessage::from_u32(stock_id) { - Some(id) => match ctx.set_stock_translation(id, msg) { + let ctx = &*context; + + block_on(async move { + match StockMessage::from_u32(stock_id) { + Some(id) => match ctx.set_stock_translation(id, msg).await { Ok(()) => 1, Err(err) => { warn!(ctx, "set_stock_translation failed: {}", err); @@ -392,8 +182,8 @@ pub unsafe extern "C" fn dc_set_stock_translation( warn!(ctx, "invalid stock message id {}", stock_id); 0 } - }) - .unwrap_or(0) + } + }) } #[no_mangle] @@ -406,16 +196,17 @@ pub unsafe extern "C" fn dc_set_config_from_qr( return 0; } let qr = to_string_lossy(qr); - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) { + let ctx = &*context; + + block_on(async move { + match qr::set_config_from_qr(&ctx, &qr).await { Ok(()) => 1, Err(err) => { error!(ctx, "Failed to create account from QR code: {}", err); 0 } - }) - .unwrap_or(0) + } + }) } #[no_mangle] @@ -424,13 +215,11 @@ pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c eprintln!("ignoring careless call to dc_get_info()"); return "".strdup(); } - let ffi_context = &*context; - let guard = ffi_context.inner.read().unwrap(); - let info = match guard.as_ref() { - Some(ref ctx) => ctx.get_info(), - None => context::get_info(), - }; - render_info(info).unwrap_or_default().strdup() + let ctx = &*context; + block_on(async move { + let info = ctx.get_info().await; + render_info(info).unwrap_or_default().strdup() + }) } fn render_info( @@ -454,15 +243,16 @@ pub unsafe extern "C" fn dc_get_oauth2_url( eprintln!("ignoring careless call to dc_get_oauth2_url()"); return ptr::null_mut(); // NULL explicitly defined as "unknown" } - let ffi_context = &*context; + let ctx = &*context; let addr = to_string_lossy(addr); let redirect = to_string_lossy(redirect); - ffi_context - .with_inner(|ctx| match oauth2::dc_get_oauth2_url(ctx, addr, redirect) { + + block_on(async move { + match oauth2::dc_get_oauth2_url(&ctx, addr, redirect).await { Some(res) => res.strdup(), None => ptr::null_mut(), - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -471,8 +261,10 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_configure()"); return; } - let ffi_context = &*context; - ffi_context.with_inner(|ctx| ctx.configure()).unwrap_or(()) + + let ctx = &*context; + + spawn(async move { ctx.configure().await.log_err(ctx, "Configure failed") }); } #[no_mangle] @@ -481,191 +273,229 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c eprintln!("ignoring careless call to dc_is_configured()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| ctx.is_configured() as libc::c_int) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { ctx.is_configured().await as libc::c_int }) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_imap_jobs(context: *mut dc_context_t) { +pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) { if context.is_null() { - eprintln!("ignoring careless call to dc_perform_imap_jobs()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_inbox_jobs(ctx)) - .unwrap_or(()) + let ctx = &*context; + + block_on({ ctx.start_io() }) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_imap_fetch(context: *mut dc_context_t) { +pub unsafe extern "C" fn dc_is_io_running(context: *mut dc_context_t) -> libc::c_int { if context.is_null() { - eprintln!("ignoring careless call to dc_perform_imap_fetch()"); - return; + return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_inbox_fetch(ctx)) - .unwrap_or(()) + let ctx = &*context; + + block_on({ ctx.is_io_running() }) as libc::c_int } #[no_mangle] -pub unsafe extern "C" fn dc_perform_imap_idle(context: *mut dc_context_t) { - // TODO rename function in co-ordination with UIs - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_imap_idle()"); +pub type dc_event_t = Event; + +#[no_mangle] +pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) { + if a.is_null() { + eprintln!("ignoring careless call to dc_event_unref()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_inbox_idle(ctx)) - .unwrap_or(()) + + Box::from_raw(a); } #[no_mangle] -pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_interrupt_imap_idle()"); - return; +pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int { + if event.is_null() { + eprintln!("ignoring careless call to dc_event_get_id()"); + return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::interrupt_inbox_idle(ctx)) - .unwrap_or(()) + + let event = &*event; + event.as_id() } #[no_mangle] -pub unsafe extern "C" fn dc_perform_mvbox_fetch(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_mvbox_fetch()"); - return; +pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc::c_int { + if event.is_null() { + eprintln!("ignoring careless call to dc_event_get_data1_int()"); + return 0; + } + + let event = &*event; + match event { + Event::Info(_) + | Event::SmtpConnected(_) + | Event::ImapConnected(_) + | Event::SmtpMessageSent(_) + | Event::ImapMessageDeleted(_) + | Event::ImapMessageMoved(_) + | Event::ImapFolderEmptied(_) + | Event::NewBlobFile(_) + | Event::DeletedBlobFile(_) + | Event::Warning(_) + | Event::Error(_) + | Event::ErrorNetwork(_) + | Event::ErrorSelfNotInGroup(_) => 0, + Event::MsgsChanged { chat_id, .. } + | Event::IncomingMsg { chat_id, .. } + | Event::MsgDelivered { chat_id, .. } + | Event::MsgFailed { chat_id, .. } + | Event::MsgRead { chat_id, .. } + | Event::ChatModified(chat_id) => chat_id.to_u32() as libc::c_int, + Event::ContactsChanged(id) | Event::LocationChanged(id) => { + let id = id.unwrap_or_default(); + id as libc::c_int + } + Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => { + *progress as libc::c_int + } + Event::ImexFileWritten(_) => 0, + Event::SecurejoinInviterProgress { contact_id, .. } + | Event::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int, } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_mvbox_fetch(ctx)) - .unwrap_or(()) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_mvbox_jobs(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_mvbox_jobs()"); - return; +pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc::c_int { + if event.is_null() { + eprintln!("ignoring careless call to dc_event_get_data2_int()"); + return 0; + } + + let event = &*event; + + match event { + Event::Info(_) + | Event::SmtpConnected(_) + | Event::ImapConnected(_) + | Event::SmtpMessageSent(_) + | Event::ImapMessageDeleted(_) + | Event::ImapMessageMoved(_) + | Event::ImapFolderEmptied(_) + | Event::NewBlobFile(_) + | Event::DeletedBlobFile(_) + | Event::Warning(_) + | Event::Error(_) + | Event::ErrorNetwork(_) + | Event::ErrorSelfNotInGroup(_) + | Event::ContactsChanged(_) + | Event::LocationChanged(_) + | Event::ConfigureProgress(_) + | Event::ImexProgress(_) + | Event::ImexFileWritten(_) + | Event::ChatModified(_) => 0, + Event::MsgsChanged { msg_id, .. } + | Event::IncomingMsg { msg_id, .. } + | Event::MsgDelivered { msg_id, .. } + | Event::MsgFailed { msg_id, .. } + | Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int, + Event::SecurejoinInviterProgress { progress, .. } + | Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int, } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_mvbox_jobs(ctx)) - .unwrap_or(()) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_mvbox_idle(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_mvbox_idle()"); - return; +pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::c_char { + if event.is_null() { + eprintln!("ignoring careless call to dc_event_get_data2_str()"); + return ptr::null_mut(); + } + + let event = &*event; + + match event { + Event::Info(msg) + | Event::SmtpConnected(msg) + | Event::ImapConnected(msg) + | Event::SmtpMessageSent(msg) + | Event::ImapMessageDeleted(msg) + | Event::ImapMessageMoved(msg) + | Event::ImapFolderEmptied(msg) + | Event::NewBlobFile(msg) + | Event::DeletedBlobFile(msg) + | Event::Warning(msg) + | Event::Error(msg) + | Event::ErrorNetwork(msg) + | Event::ErrorSelfNotInGroup(msg) => { + let data2 = msg.to_c_string().unwrap_or_default(); + data2.into_raw() + } + Event::MsgsChanged { .. } + | Event::IncomingMsg { .. } + | Event::MsgDelivered { .. } + | Event::MsgFailed { .. } + | Event::MsgRead { .. } + | Event::ChatModified(_) + | Event::ContactsChanged(_) + | Event::LocationChanged(_) + | Event::ConfigureProgress(_) + | Event::ImexProgress(_) + | Event::SecurejoinInviterProgress { .. } + | Event::SecurejoinJoinerProgress { .. } => ptr::null_mut(), + Event::ImexFileWritten(file) => { + let data2 = file.to_c_string().unwrap_or_default(); + data2.into_raw() + } } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_mvbox_idle(ctx)) - .unwrap_or(()) } #[no_mangle] -pub unsafe extern "C" fn dc_interrupt_mvbox_idle(context: *mut dc_context_t) { +pub type dc_event_emitter_t = EventEmitter; + +#[no_mangle] +pub unsafe extern "C" fn dc_get_event_emitter( + context: *mut dc_context_t, +) -> *mut dc_event_emitter_t { if context.is_null() { - eprintln!("ignoring careless call to dc_interrupt_mvbox_idle()"); - return; + eprintln!("ignoring careless call to dc_get_event_emitter()"); + return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::interrupt_mvbox_idle(ctx)) - .unwrap_or(()) + let ctx = &*context; + Box::into_raw(Box::new(ctx.get_event_emitter())) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_sentbox_fetch(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_sentbox_fetch()"); +pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t) { + if emitter.is_null() { + eprintln!("ignoring careless call to dc_event_mitter_unref()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_sentbox_fetch(ctx)) - .unwrap_or(()) + + Box::from_raw(emitter); } #[no_mangle] -pub unsafe extern "C" fn dc_perform_sentbox_jobs(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_sentbox_jobs()"); - return; +pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *mut dc_event_t { + if events.is_null() { + return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_sentbox_jobs(ctx)) - .unwrap_or(()) + let events = &*events; + + events + .recv_sync() + .map(|ev| Box::into_raw(Box::new(ev))) + .unwrap_or_else(|| ptr::null_mut()) } #[no_mangle] -pub unsafe extern "C" fn dc_perform_sentbox_idle(context: *mut dc_context_t) { +pub unsafe extern "C" fn dc_stop_io(context: *mut dc_context_t) { if context.is_null() { - eprintln!("ignoring careless call to dc_perform_sentbox_idle()"); + eprintln!("ignoring careless call to dc_shutdown()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_sentbox_idle(ctx)) - .unwrap_or(()) -} + let ctx = &*context; -#[no_mangle] -pub unsafe extern "C" fn dc_interrupt_sentbox_idle(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_interrupt_sentbox_idle()"); - return; - } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::interrupt_sentbox_idle(ctx)) - .unwrap_or(()) -} - -#[no_mangle] -pub unsafe extern "C" fn dc_perform_smtp_jobs(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_smtp_jobs()"); - return; - } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_smtp_jobs(ctx)) - .unwrap_or(()) -} - -#[no_mangle] -pub unsafe extern "C" fn dc_perform_smtp_idle(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_perform_smtp_idle()"); - return; - } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::perform_smtp_idle(ctx)) - .unwrap_or(()) -} - -#[no_mangle] -pub unsafe extern "C" fn dc_interrupt_smtp_idle(context: *mut dc_context_t) { - if context.is_null() { - eprintln!("ignoring careless call to dc_interrupt_smtp_idle()"); - return; - } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::interrupt_smtp_idle(ctx)) - .unwrap_or(()) + block_on(async move { + ctx.stop_io().await; + }) } #[no_mangle] @@ -674,10 +504,9 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_maybe_network()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| job::maybe_network(ctx)) - .unwrap_or(()) + let ctx = &*context; + + block_on(async move { ctx.maybe_network().await }) } #[no_mangle] @@ -691,22 +520,21 @@ pub unsafe extern "C" fn dc_preconfigure_keypair( eprintln!("ignoring careless call to dc_preconfigure_keypair()"); return 0; } - let ffi_context = &*context; - ffi_context - .try_inner(|ctx| { - let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?; - let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?; - let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?; - let keypair = key::KeyPair { - addr, - public, - secret, - }; - key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?; - Ok(1) - }) - .log_err(ffi_context, "Failed to save keypair") - .unwrap_or(0) + let ctx = &*context; + block_on(async move { + let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?; + let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?; + let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?; + let keypair = key::KeyPair { + addr, + public, + secret, + }; + key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default).await?; + Ok::<_, anyhow::Error>(1) + }) + .log_err(ctx, "Failed to save keypair") + .unwrap_or(0) } #[no_mangle] @@ -720,26 +548,27 @@ pub unsafe extern "C" fn dc_get_chatlist( eprintln!("ignoring careless call to dc_get_chatlist()"); return ptr::null_mut(); } - let ffi_context = &*context; + let ctx = &*context; let qs = to_opt_string_lossy(query_str); let qi = if query_id == 0 { None } else { Some(query_id) }; - ffi_context - .with_inner(|ctx| { - match chatlist::Chatlist::try_load( - ctx, - flags as usize, - qs.as_ref().map(|x| x.as_str()), - qi, - ) { - Ok(list) => { - let ffi_list = ChatlistWrapper { context, list }; - Box::into_raw(Box::new(ffi_list)) - } - Err(_) => ptr::null_mut(), + + block_on(async move { + match chatlist::Chatlist::try_load( + &ctx, + flags as usize, + qs.as_ref().map(|x| x.as_str()), + qi, + ) + .await + { + Ok(list) => { + let ffi_list = ChatlistWrapper { context, list }; + Box::into_raw(Box::new(ffi_list)) } - }) - .unwrap_or_else(|_| ptr::null_mut()) + Err(_) => ptr::null_mut(), + } + }) } #[no_mangle] @@ -748,15 +577,15 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms eprintln!("ignoring careless call to dc_create_chat_by_msg_id()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::create_by_msg_id(ctx, MsgId::new(msg_id)) - .log_err(ffi_context, "Failed to create chat from msg_id") - .map(|id| id.to_u32()) - .unwrap_or(0) - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::create_by_msg_id(&ctx, MsgId::new(msg_id)) + .await + .log_err(ctx, "Failed to create chat from msg_id") + .map(|id| id.to_u32()) + .unwrap_or(0) + }) } #[no_mangle] @@ -768,15 +597,15 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id( eprintln!("ignoring careless call to dc_create_chat_by_contact_id()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::create_by_contact_id(ctx, contact_id) - .log_err(ffi_context, "Failed to create chat from contact_id") - .map(|id| id.to_u32()) - .unwrap_or(0) - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::create_by_contact_id(&ctx, contact_id) + .await + .log_err(ctx, "Failed to create chat from contact_id") + .map(|id| id.to_u32()) + .unwrap_or(0) + }) } #[no_mangle] @@ -788,15 +617,15 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id( eprintln!("ignoring careless call to dc_get_chat_id_by_contact_id()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::get_by_contact_id(ctx, contact_id) - .log_err(ffi_context, "Failed to get chat for contact_id") - .map(|id| id.to_u32()) - .unwrap_or(0) - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::get_by_contact_id(&ctx, contact_id) + .await + .log_err(ctx, "Failed to get chat for contact_id") + .map(|id| id.to_u32()) + .unwrap_or(0) + }) } #[no_mangle] @@ -809,15 +638,15 @@ pub unsafe extern "C" fn dc_prepare_msg( eprintln!("ignoring careless call to dc_prepare_msg()"); return 0; } - let ffi_context = &mut *context; + let ctx = &mut *context; let ffi_msg: &mut MessageWrapper = &mut *msg; - ffi_context - .with_inner(|ctx| { - chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message) - .unwrap_or_log_default(ctx, "Failed to prepare message") - }) - .map(|msg_id| msg_id.to_u32()) - .unwrap_or(0) + + block_on(async move { + chat::prepare_msg(&ctx, ChatId::new(chat_id), &mut ffi_msg.message) + .await + .unwrap_or_log_default(&ctx, "Failed to prepare message") + }) + .to_u32() } #[no_mangle] @@ -830,15 +659,36 @@ pub unsafe extern "C" fn dc_send_msg( eprintln!("ignoring careless call to dc_send_msg()"); return 0; } - let ffi_context = &mut *context; + let ctx = &mut *context; let ffi_msg = &mut *msg; - ffi_context - .with_inner(|ctx| { - chat::send_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message) - .unwrap_or_log_default(ctx, "Failed to send message") - }) - .map(|msg_id| msg_id.to_u32()) - .unwrap_or(0) + + block_on(async move { + chat::send_msg(&ctx, ChatId::new(chat_id), &mut ffi_msg.message) + .await + .unwrap_or_log_default(&ctx, "Failed to send message") + }) + .to_u32() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_send_msg_sync( + context: *mut dc_context_t, + chat_id: u32, + msg: *mut dc_msg_t, +) -> u32 { + if context.is_null() || msg.is_null() { + eprintln!("ignoring careless call to dc_send_msg_sync()"); + return 0; + } + let ctx = &mut *context; + let ffi_msg = &mut *msg; + + block_on(async move { + chat::send_msg_sync(&ctx, ChatId::new(chat_id), &mut ffi_msg.message) + .await + .unwrap_or_log_default(&ctx, "Failed to send message") + }) + .to_u32() } #[no_mangle] @@ -851,15 +701,15 @@ pub unsafe extern "C" fn dc_send_text_msg( eprintln!("ignoring careless call to dc_send_text_msg()"); return 0; } - let ffi_context = &*context; + let ctx = &*context; let text_to_send = to_string_lossy(text_to_send); - ffi_context - .with_inner(|ctx| { - chat::send_text_msg(ctx, ChatId::new(chat_id), text_to_send) - .map(|msg_id| msg_id.to_u32()) - .unwrap_or_log_default(ctx, "Failed to send text message") - }) - .unwrap_or(0) + + block_on(async move { + chat::send_text_msg(&ctx, ChatId::new(chat_id), text_to_send) + .await + .map(|msg_id| msg_id.to_u32()) + .unwrap_or_log_default(&ctx, "Failed to send text message") + }) } #[no_mangle] @@ -872,16 +722,15 @@ pub unsafe extern "C" fn dc_set_draft( eprintln!("ignoring careless call to dc_set_draft()"); return; } - let ffi_context = &*context; + let ctx = &*context; let msg = if msg.is_null() { None } else { let ffi_msg: &mut MessageWrapper = &mut *msg; Some(&mut ffi_msg.message) }; - ffi_context - .with_inner(|ctx| ChatId::new(chat_id).set_draft(ctx, msg)) - .unwrap_or(()) + + block_on(ChatId::new(chat_id).set_draft(&ctx, msg)) } #[no_mangle] @@ -894,24 +743,24 @@ pub unsafe extern "C" fn dc_add_device_msg( eprintln!("ignoring careless call to dc_add_device_msg()"); return 0; } - let ffi_context = &mut *context; + let ctx = &mut *context; let msg = if msg.is_null() { None } else { let ffi_msg: &mut MessageWrapper = &mut *msg; Some(&mut ffi_msg.message) }; - ffi_context - .with_inner(|ctx| { - chat::add_device_msg( - ctx, - to_opt_string_lossy(label).as_ref().map(|x| x.as_str()), - msg, - ) - .unwrap_or_log_default(ctx, "Failed to add device message") - }) - .map(|msg_id| msg_id.to_u32()) - .unwrap_or(0) + + block_on(async move { + chat::add_device_msg( + &ctx, + to_opt_string_lossy(label).as_ref().map(|x| x.as_str()), + msg, + ) + .await + .unwrap_or_log_default(&ctx, "Failed to add device message") + }) + .to_u32() } #[no_mangle] @@ -920,13 +769,13 @@ pub unsafe extern "C" fn dc_update_device_chats(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_update_device_chats()"); return; } - let ffi_context = &mut *context; - ffi_context - .with_inner(|ctx| { - ctx.update_device_chats() - .unwrap_or_log_default(ctx, "Failed to add device message") - }) - .unwrap_or(()) + let ctx = &mut *context; + + block_on(async move { + ctx.update_device_chats() + .await + .unwrap_or_log_default(&ctx, "Failed to add device message") + }) } #[no_mangle] @@ -938,13 +787,13 @@ pub unsafe extern "C" fn dc_was_device_msg_ever_added( eprintln!("ignoring careless call to dc_was_device_msg_ever_added()"); return 0; } - let ffi_context = &mut *context; - ffi_context - .with_inner(|ctx| { - chat::was_device_msg_ever_added(ctx, &to_string_lossy(label)).unwrap_or(false) - as libc::c_int - }) - .unwrap_or(0) + let ctx = &mut *context; + + block_on(async move { + chat::was_device_msg_ever_added(&ctx, &to_string_lossy(label)) + .await + .unwrap_or(false) as libc::c_int + }) } #[no_mangle] @@ -953,9 +802,10 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32) eprintln!("ignoring careless call to dc_get_draft()"); return ptr::null_mut(); // NULL explicitly defined as "no draft" } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match ChatId::new(chat_id).get_draft(ctx) { + let ctx = &*context; + + block_on(async move { + match ChatId::new(chat_id).get_draft(&ctx).await { Ok(Some(draft)) => { let ffi_msg = MessageWrapper { context, @@ -968,8 +818,8 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32) error!(ctx, "Failed to get draft for chat #{}: {}", chat_id, err); ptr::null_mut() } - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -983,23 +833,23 @@ pub unsafe extern "C" fn dc_get_chat_msgs( eprintln!("ignoring careless call to dc_get_chat_msgs()"); return ptr::null_mut(); } - let ffi_context = &*context; + let ctx = &*context; let marker_flag = if marker1before <= DC_MSG_ID_LAST_SPECIAL { None } else { Some(MsgId::new(marker1before)) }; - ffi_context - .with_inner(|ctx| { - let arr = dc_array_t::from( - chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag) - .iter() - .map(|msg_id| msg_id.to_u32()) - .collect::>(), - ); - Box::into_raw(Box::new(arr)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + + block_on(async move { + let arr = dc_array_t::from( + chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag) + .await + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) + }) } #[no_mangle] @@ -1008,10 +858,9 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32 eprintln!("ignoring careless call to dc_get_msg_cnt()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| ChatId::new(chat_id).get_msg_cnt(ctx) as libc::c_int) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { ChatId::new(chat_id).get_msg_cnt(&ctx).await as libc::c_int }) } #[no_mangle] @@ -1023,10 +872,9 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt( eprintln!("ignoring careless call to dc_get_fresh_msg_cnt()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| ChatId::new(chat_id).get_fresh_msg_cnt(ctx) as libc::c_int) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await as libc::c_int }) } #[no_mangle] @@ -1039,13 +887,12 @@ pub unsafe extern "C" fn dc_estimate_deletion_cnt( eprintln!("ignoring careless call to dc_estimate_deletion_cnt()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0) - as libc::c_int - }) - .unwrap_or(0) + let ctx = &*context; + block_on(async move { + message::estimate_deletion_cnt(ctx, from_server != 0, seconds) + .await + .unwrap_or(0) as libc::c_int + }) } #[no_mangle] @@ -1056,18 +903,18 @@ pub unsafe extern "C" fn dc_get_fresh_msgs( eprintln!("ignoring careless call to dc_get_fresh_msgs()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let arr = dc_array_t::from( - ctx.get_fresh_msgs() - .iter() - .map(|msg_id| msg_id.to_u32()) - .collect::>(), - ); - Box::into_raw(Box::new(arr)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + let arr = dc_array_t::from( + ctx.get_fresh_msgs() + .await + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) + }) } #[no_mangle] @@ -1076,14 +923,14 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id eprintln!("ignoring careless call to dc_marknoticed_chat()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::marknoticed_chat(ctx, ChatId::new(chat_id)) - .log_err(ffi_context, "Failed marknoticed chat") - .unwrap_or(()) - }) - .unwrap_or(()) + let ctx = &*context; + + block_on(async move { + chat::marknoticed_chat(&ctx, ChatId::new(chat_id)) + .await + .log_err(ctx, "Failed marknoticed chat") + .unwrap_or(()) + }) } #[no_mangle] @@ -1092,14 +939,14 @@ pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_marknoticed_all_chats()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::marknoticed_all_chats(ctx) - .log_err(ffi_context, "Failed marknoticed all chats") - .unwrap_or(()) - }) - .unwrap_or(()) + let ctx = &*context; + + block_on(async move { + chat::marknoticed_all_chats(&ctx) + .await + .log_err(ctx, "Failed marknoticed all chats") + .unwrap_or(()) + }) } fn from_prim(s: S) -> Option @@ -1122,29 +969,29 @@ pub unsafe extern "C" fn dc_get_chat_media( eprintln!("ignoring careless call to dc_get_chat_media()"); return ptr::null_mut(); } - let ffi_context = &*context; + let ctx = &*context; let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type)); let or_msg_type2 = from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2)); let or_msg_type3 = from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3)); - ffi_context - .with_inner(|ctx| { - let arr = dc_array_t::from( - chat::get_chat_media( - ctx, - ChatId::new(chat_id), - msg_type, - or_msg_type2, - or_msg_type3, - ) - .iter() - .map(|msg_id| msg_id.to_u32()) - .collect::>(), - ); - Box::into_raw(Box::new(arr)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + + block_on(async move { + let arr = dc_array_t::from( + chat::get_chat_media( + &ctx, + ChatId::new(chat_id), + msg_type, + or_msg_type2, + or_msg_type3, + ) + .await + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) + }) } #[no_mangle] @@ -1166,26 +1013,26 @@ pub unsafe extern "C" fn dc_get_next_media( chat::Direction::Forward }; - let ffi_context = &*context; + let ctx = &*context; let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type)); let or_msg_type2 = from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2)); let or_msg_type3 = from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3)); - ffi_context - .with_inner(|ctx| { - chat::get_next_media( - ctx, - MsgId::new(msg_id), - direction, - msg_type, - or_msg_type2, - or_msg_type3, - ) - .map(|msg_id| msg_id.to_u32()) - .unwrap_or(0) - }) + + block_on(async move { + chat::get_next_media( + &ctx, + MsgId::new(msg_id), + direction, + msg_type, + or_msg_type2, + or_msg_type3, + ) + .await + .map(|msg_id| msg_id.to_u32()) .unwrap_or(0) + }) } #[no_mangle] @@ -1198,26 +1045,27 @@ pub unsafe extern "C" fn dc_set_chat_visibility( eprintln!("ignoring careless call to dc_set_chat_visibility()"); return; } - let ffi_context = &*context; + let ctx = &*context; let visibility = match archive { 0 => ChatVisibility::Normal, 1 => ChatVisibility::Archived, 2 => ChatVisibility::Pinned, _ => { - ffi_context.warning( + warn!( + ctx, "ignoring careless call to dc_set_chat_visibility(): unknown archived state", ); return; } }; - ffi_context - .with_inner(|ctx| { - ChatId::new(chat_id) - .set_visibility(ctx, visibility) - .log_err(ffi_context, "Failed setting chat visibility") - .unwrap_or(()) - }) - .unwrap_or(()) + + block_on(async move { + ChatId::new(chat_id) + .set_visibility(&ctx, visibility) + .await + .log_err(ctx, "Failed setting chat visibility") + .unwrap_or(()) + }) } #[no_mangle] @@ -1226,15 +1074,15 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32 eprintln!("ignoring careless call to dc_delete_chat()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - ChatId::new(chat_id) - .delete(ctx) - .log_err(ffi_context, "Failed chat delete") - .unwrap_or(()) - }) - .unwrap_or(()) + let ctx = &*context; + + block_on(async move { + ChatId::new(chat_id) + .delete(&ctx) + .await + .log_err(ctx, "Failed chat delete") + .unwrap_or(()) + }) } #[no_mangle] @@ -1246,13 +1094,12 @@ pub unsafe extern "C" fn dc_get_chat_contacts( eprintln!("ignoring careless call to dc_get_chat_contacts()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let arr = dc_array_t::from(chat::get_chat_contacts(ctx, ChatId::new(chat_id))); - Box::into_raw(Box::new(arr)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await); + Box::into_raw(Box::new(arr)) + }) } #[no_mangle] @@ -1265,18 +1112,18 @@ pub unsafe extern "C" fn dc_search_msgs( eprintln!("ignoring careless call to dc_search_msgs()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let arr = dc_array_t::from( - ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query)) - .iter() - .map(|msg_id| msg_id.to_u32()) - .collect::>(), - ); - Box::into_raw(Box::new(arr)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + let arr = dc_array_t::from( + ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query)) + .await + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) + }) } #[no_mangle] @@ -1285,18 +1132,17 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) - eprintln!("ignoring careless call to dc_get_chat()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner( - |ctx| match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)) { - Ok(chat) => { - let ffi_chat = ChatWrapper { context, chat }; - Box::into_raw(Box::new(ffi_chat)) - } - Err(_) => ptr::null_mut(), - }, - ) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + match chat::Chat::load_from_db(&ctx, ChatId::new(chat_id)).await { + Ok(chat) => { + let ffi_chat = ChatWrapper { context, chat }; + Box::into_raw(Box::new(ffi_chat)) + } + Err(_) => ptr::null_mut(), + } + }) } #[no_mangle] @@ -1309,20 +1155,20 @@ pub unsafe extern "C" fn dc_create_group_chat( eprintln!("ignoring careless call to dc_create_group_chat()"); return 0; } - let ffi_context = &*context; + let ctx = &*context; let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) { s } else { return 0; }; - ffi_context - .with_inner(|ctx| { - chat::create_group_chat(ctx, verified, to_string_lossy(name)) - .log_err(ffi_context, "Failed to create group chat") - .map(|id| id.to_u32()) - .unwrap_or(0) - }) - .unwrap_or(0) + + block_on(async move { + chat::create_group_chat(&ctx, verified, to_string_lossy(name)) + .await + .log_err(ctx, "Failed to create group chat") + .map(|id| id.to_u32()) + .unwrap_or(0) + }) } #[no_mangle] @@ -1335,10 +1181,9 @@ pub unsafe extern "C" fn dc_is_contact_in_chat( eprintln!("ignoring careless call to dc_is_contact_in_chat()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| chat::is_contact_in_chat(ctx, ChatId::new(chat_id), contact_id)) - .unwrap_or_default() + let ctx = &*context; + + block_on(async move { chat::is_contact_in_chat(&ctx, ChatId::new(chat_id), contact_id).await }) .into() } @@ -1352,12 +1197,11 @@ pub unsafe extern "C" fn dc_add_contact_to_chat( eprintln!("ignoring careless call to dc_add_contact_to_chat()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::add_contact_to_chat(ctx, ChatId::new(chat_id), contact_id) as libc::c_int - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::add_contact_to_chat(&ctx, ChatId::new(chat_id), contact_id).await as libc::c_int + }) } #[no_mangle] @@ -1370,14 +1214,14 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat( eprintln!("ignoring careless call to dc_remove_contact_from_chat()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::remove_contact_from_chat(ctx, ChatId::new(chat_id), contact_id) - .map(|_| 1) - .unwrap_or_log_default(ctx, "Failed to remove contact") - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::remove_contact_from_chat(&ctx, ChatId::new(chat_id), contact_id) + .await + .map(|_| 1) + .unwrap_or_log_default(&ctx, "Failed to remove contact") + }) } #[no_mangle] @@ -1390,14 +1234,14 @@ pub unsafe extern "C" fn dc_set_chat_name( eprintln!("ignoring careless call to dc_set_chat_name()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::set_chat_name(ctx, ChatId::new(chat_id), to_string_lossy(name)) - .map(|_| 1) - .unwrap_or_log_default(ctx, "Failed to set chat name") - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::set_chat_name(&ctx, ChatId::new(chat_id), to_string_lossy(name)) + .await + .map(|_| 1) + .unwrap_or_log_default(&ctx, "Failed to set chat name") + }) } #[no_mangle] @@ -1410,14 +1254,14 @@ pub unsafe extern "C" fn dc_set_chat_profile_image( eprintln!("ignoring careless call to dc_set_chat_profile_image()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image)) - .map(|_| 1) - .unwrap_or_log_default(ctx, "Failed to set profile image") - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), to_string_lossy(image)) + .await + .map(|_| 1) + .unwrap_or_log_default(&ctx, "Failed to set profile image") + }) } #[no_mangle] @@ -1430,25 +1274,26 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration( eprintln!("ignoring careless call to dc_set_chat_mute_duration()"); return 0; } - let ffi_context = &*context; + let ctx = &*context; let muteDuration = match duration { 0 => MuteDuration::NotMuted, -1 => MuteDuration::Forever, n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)), _ => { - ffi_context.warning( + warn!( + ctx, "dc_chat_set_mute_duration(): Can not use negative duration other than -1", ); return 0; } }; - ffi_context - .with_inner(|ctx| { - chat::set_muted(ctx, ChatId::new(chat_id), muteDuration) - .map(|_| 1) - .unwrap_or_log_default(ctx, "Failed to set mute duration") - }) - .unwrap_or(0) + + block_on(async move { + chat::set_muted(&ctx, ChatId::new(chat_id), muteDuration) + .await + .map(|_| 1) + .unwrap_or_log_default(&ctx, "Failed to set mute duration") + }) } #[no_mangle] @@ -1460,10 +1305,9 @@ pub unsafe extern "C" fn dc_get_msg_info( eprintln!("ignoring careless call to dc_get_msg_info()"); return "".strdup(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| message::get_msg_info(ctx, MsgId::new(msg_id)).strdup()) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup() } #[no_mangle] @@ -1475,14 +1319,14 @@ pub unsafe extern "C" fn dc_get_mime_headers( eprintln!("ignoring careless call to dc_get_mime_headers()"); return ptr::null_mut(); // NULL explicitly defined as "no mime headers" } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - message::get_mime_headers(ctx, MsgId::new(msg_id)) - .map(|s| s.strdup()) - .unwrap_or_else(ptr::null_mut) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + message::get_mime_headers(&ctx, MsgId::new(msg_id)) + .await + .map(|s| s.strdup()) + .unwrap_or_else(ptr::null_mut) + }) } #[no_mangle] @@ -1495,11 +1339,10 @@ pub unsafe extern "C" fn dc_delete_msgs( eprintln!("ignoring careless call to dc_delete_msgs()"); return; } - let ffi_context = &*context; + let ctx = &*context; let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); - ffi_context - .with_inner(|ctx| message::delete_msgs(ctx, &msg_ids[..])) - .unwrap_or(()) + + block_on(message::delete_msgs(&ctx, &msg_ids)) } #[no_mangle] @@ -1508,10 +1351,9 @@ pub unsafe extern "C" fn dc_empty_server(context: *mut dc_context_t, flags: u32) eprintln!("ignoring careless call to dc_empty_server()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| message::dc_empty_server(ctx, flags)) - .unwrap_or(()) + let ctx = &*context; + + block_on(message::dc_empty_server(&ctx, flags)) } #[no_mangle] @@ -1530,13 +1372,13 @@ pub unsafe extern "C" fn dc_forward_msgs( return; } let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - chat::forward_msgs(ctx, &msg_ids[..], ChatId::new(chat_id)) - .unwrap_or_log_default(ctx, "Failed to forward message") - }) - .unwrap_or_default() + let ctx = &*context; + + block_on(async move { + chat::forward_msgs(&ctx, &msg_ids[..], ChatId::new(chat_id)) + .await + .unwrap_or_log_default(&ctx, "Failed to forward message") + }) } #[no_mangle] @@ -1545,10 +1387,9 @@ pub unsafe extern "C" fn dc_marknoticed_contact(context: *mut dc_context_t, cont eprintln!("ignoring careless call to dc_marknoticed_contact()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| Contact::mark_noticed(ctx, contact_id)) - .unwrap_or(()) + let ctx = &*context; + + block_on(Contact::mark_noticed(&ctx, contact_id)) } #[no_mangle] @@ -1562,10 +1403,9 @@ pub unsafe extern "C" fn dc_markseen_msgs( return; } let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| message::markseen_msgs(ctx, &msg_ids[..])) - .ok(); + let ctx = &*context; + + block_on(message::markseen_msgs(&ctx, msg_ids)); } #[no_mangle] @@ -1580,10 +1420,9 @@ pub unsafe extern "C" fn dc_star_msgs( return; } let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| message::star_msgs(ctx, &msg_ids[..], star == 1)) - .ok(); + let ctx = &*context; + + block_on(message::star_msgs(&ctx, msg_ids, star == 1)); } #[no_mangle] @@ -1592,32 +1431,31 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) -> eprintln!("ignoring careless call to dc_get_msg()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)) { - Ok(msg) => msg, - Err(e) => { - if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL { - // C-core API returns empty messages, do the same - warn!( - ctx, - "dc_get_msg called with special msg_id={}, returning empty msg", msg_id - ); - message::Message::default() - } else { - error!( - ctx, - "dc_get_msg could not retrieve msg_id {}: {}", msg_id, e - ); - return ptr::null_mut(); - } + let ctx = &*context; + + block_on(async move { + let message = match message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await { + Ok(msg) => msg, + Err(e) => { + if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL { + // C-core API returns empty messages, do the same + warn!( + &ctx, + "dc_get_msg called with special msg_id={}, returning empty msg", msg_id + ); + message::Message::default() + } else { + error!( + &ctx, + "dc_get_msg could not retrieve msg_id {}: {}", msg_id, e + ); + return ptr::null_mut(); } - }; - let ffi_msg = MessageWrapper { context, message }; - Box::into_raw(Box::new(ffi_msg)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }; + let ffi_msg = MessageWrapper { context, message }; + Box::into_raw(Box::new(ffi_msg)) + }) } #[no_mangle] @@ -1639,12 +1477,13 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr( eprintln!("ignoring careless call to dc_lookup_contact_id_by_addr()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo) - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(Contact::lookup_id_by_addr( + &ctx, + to_string_lossy(addr), + Origin::IncomingReplyTo, + )) } #[no_mangle] @@ -1657,16 +1496,15 @@ pub unsafe extern "C" fn dc_create_contact( eprintln!("ignoring careless call to dc_create_contact()"); return 0; } - let ffi_context = &*context; + let ctx = &*context; let name = to_string_lossy(name); - ffi_context - .with_inner( - |ctx| match Contact::create(ctx, name, to_string_lossy(addr)) { - Ok(id) => id, - Err(_) => 0, - }, - ) - .unwrap_or(0) + + block_on(async move { + match Contact::create(&ctx, name, to_string_lossy(addr)).await { + Ok(id) => id, + Err(_) => 0, + } + }) } #[no_mangle] @@ -1678,15 +1516,14 @@ pub unsafe extern "C" fn dc_add_address_book( eprintln!("ignoring careless call to dc_add_address_book()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner( - |ctx| match Contact::add_address_book(ctx, to_string_lossy(addr_book)) { - Ok(cnt) => cnt as libc::c_int, - Err(_) => 0, - }, - ) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { + match Contact::add_address_book(&ctx, to_string_lossy(addr_book)).await { + Ok(cnt) => cnt as libc::c_int, + Err(_) => 0, + } + }) } #[no_mangle] @@ -1699,14 +1536,15 @@ pub unsafe extern "C" fn dc_get_contacts( eprintln!("ignoring careless call to dc_get_contacts()"); return ptr::null_mut(); } - let ffi_context = &*context; + let ctx = &*context; let query = to_opt_string_lossy(query); - ffi_context - .with_inner(|ctx| match Contact::get_all(ctx, flags, query) { + + block_on(async move { + match Contact::get_all(&ctx, flags, query).await { Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))), Err(_) => ptr::null_mut(), - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -1715,10 +1553,9 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc: eprintln!("ignoring careless call to dc_get_blocked_cnt()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| Contact::get_blocked_cnt(ctx) as libc::c_int) - .unwrap_or(0) + let ctx = &*context; + + block_on(Contact::get_blocked_cnt(&ctx)) as libc::c_int } #[no_mangle] @@ -1729,10 +1566,13 @@ pub unsafe extern "C" fn dc_get_blocked_contacts( eprintln!("ignoring careless call to dc_get_blocked_contacts()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| Box::into_raw(Box::new(dc_array_t::from(Contact::get_all_blocked(ctx))))) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + Box::into_raw(Box::new(dc_array_t::from( + Contact::get_all_blocked(&ctx).await, + ))) + }) } #[no_mangle] @@ -1745,16 +1585,14 @@ pub unsafe extern "C" fn dc_block_contact( eprintln!("ignoring careless call to dc_block_contact()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - if block == 0 { - Contact::unblock(ctx, contact_id); - } else { - Contact::block(ctx, contact_id); - } - }) - .ok(); + let ctx = &*context; + block_on(async move { + if block == 0 { + Contact::unblock(&ctx, contact_id).await; + } else { + Contact::block(&ctx, contact_id).await; + } + }); } #[no_mangle] @@ -1766,17 +1604,17 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo( eprintln!("ignoring careless call to dc_get_contact_encrinfo()"); return "".strdup(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - Contact::get_encrinfo(ctx, contact_id) - .map(|s| s.strdup()) - .unwrap_or_else(|e| { - error!(ctx, "{}", e); - ptr::null_mut() - }) - }) - .unwrap_or_else(|_| "".strdup()) + let ctx = &*context; + + block_on(async move { + Contact::get_encrinfo(&ctx, contact_id) + .await + .map(|s| s.strdup()) + .unwrap_or_else(|e| { + error!(&ctx, "{}", e); + ptr::null_mut() + }) + }) } #[no_mangle] @@ -1788,13 +1626,14 @@ pub unsafe extern "C" fn dc_delete_contact( eprintln!("ignoring careless call to dc_delete_contact()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match Contact::delete(ctx, contact_id) { + let ctx = &*context; + + block_on(async move { + match Contact::delete(&ctx, contact_id).await { Ok(_) => 1, Err(_) => 0, - }) - .unwrap_or(0) + } + }) } #[no_mangle] @@ -1806,20 +1645,20 @@ pub unsafe extern "C" fn dc_get_contact( eprintln!("ignoring careless call to dc_get_contact()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - Contact::get_by_id(ctx, contact_id) - .map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact }))) - .unwrap_or_else(|_| ptr::null_mut()) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + Contact::get_by_id(&ctx, contact_id) + .await + .map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact }))) + .unwrap_or_else(|_| ptr::null_mut()) + }) } #[no_mangle] pub unsafe extern "C" fn dc_imex( context: *mut dc_context_t, - what: libc::c_int, + what_raw: libc::c_int, param1: *const libc::c_char, _param2: *const libc::c_char, ) { @@ -1827,18 +1666,23 @@ pub unsafe extern "C" fn dc_imex( eprintln!("ignoring careless call to dc_imex()"); return; } - let what = match imex::ImexMode::from_i32(what as i32) { + let what = match imex::ImexMode::from_i32(what_raw as i32) { Some(what) => what, None => { - eprintln!("ignoring invalid argument {} to dc_imex", what); + eprintln!("ignoring invalid argument {} to dc_imex", what_raw); return; } }; - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| imex::imex(ctx, what, to_opt_string_lossy(param1))) - .ok(); + let ctx = &*context; + + let param1 = to_opt_string_lossy(param1); + + spawn(async move { + imex::imex(&ctx, what, param1) + .await + .log_err(ctx, "IMEX failed") + }); } #[no_mangle] @@ -1850,18 +1694,19 @@ pub unsafe extern "C" fn dc_imex_has_backup( eprintln!("ignoring careless call to dc_imex_has_backup()"); return ptr::null_mut(); // NULL explicitly defined as "has no backup" } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) { + let ctx = &*context; + + block_on(async move { + match imex::has_backup(&ctx, to_string_lossy(dir)).await { Ok(res) => res.strdup(), Err(err) => { // do not bubble up error to the user, // the ui will expect that the file does not exist or cannot be accessed - warn!(ctx, "dc_imex_has_backup: {}", err); + warn!(&ctx, "dc_imex_has_backup: {}", err); ptr::null_mut() } - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -1870,16 +1715,17 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) -> eprintln!("ignoring careless call to dc_initiate_key_transfer()"); return ptr::null_mut(); // NULL explicitly defined as "error" } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| match imex::initiate_key_transfer(ctx) { + let ctx = &*context; + + block_on(async move { + match imex::initiate_key_transfer(&ctx).await { Ok(res) => res.strdup(), Err(err) => { - error!(ctx, "dc_initiate_key_transfer(): {}", err); + error!(&ctx, "dc_initiate_key_transfer(): {}", err); ptr::null_mut() } - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -1895,19 +1741,19 @@ pub unsafe extern "C" fn dc_continue_key_transfer( eprintln!("ignoring careless call to dc_continue_key_transfer()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code)) - { - Ok(()) => 1, - Err(err) => { - error!(ctx, "dc_continue_key_transfer: {}", err); - 0 - } + let ctx = &*context; + + block_on(async move { + match imex::continue_key_transfer(&ctx, MsgId::new(msg_id), &to_string_lossy(setup_code)) + .await + { + Ok(()) => 1, + Err(err) => { + error!(&ctx, "dc_continue_key_transfer: {}", err); + 0 } - }) - .unwrap_or(0) + } + }) } #[no_mangle] @@ -1916,8 +1762,8 @@ pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_stop_ongoing_process()"); return; } - let ffi_context = &*context; - ffi_context.with_inner(|ctx| ctx.stop_ongoing()).ok(); + let ctx = &*context; + block_on(ctx.stop_ongoing()); } #[no_mangle] @@ -1929,13 +1775,12 @@ pub unsafe extern "C" fn dc_check_qr( eprintln!("ignoring careless call to dc_check_qr()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let lot = qr::check_qr(ctx, to_string_lossy(qr)); - Box::into_raw(Box::new(lot)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + let lot = qr::check_qr(&ctx, to_string_lossy(qr)).await; + Box::into_raw(Box::new(lot)) + }) } #[no_mangle] @@ -1947,14 +1792,14 @@ pub unsafe extern "C" fn dc_get_securejoin_qr( eprintln!("ignoring careless call to dc_get_securejoin_qr()"); return "".strdup(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - securejoin::dc_get_securejoin_qr(ctx, ChatId::new(chat_id)) - .unwrap_or_else(|| "".to_string()) - .strdup() - }) - .unwrap_or_else(|_| "".strdup()) + let ctx = &*context; + + block_on(async move { + securejoin::dc_get_securejoin_qr(&ctx, ChatId::new(chat_id)) + .await + .unwrap_or_else(|| "".to_string()) + .strdup() + }) } #[no_mangle] @@ -1966,10 +1811,10 @@ pub unsafe extern "C" fn dc_join_securejoin( eprintln!("ignoring careless call to dc_join_securejoin()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr)).to_u32()) - .unwrap_or(0) + let ctx = &*context; + + block_on(async move { securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr)).await }) + .to_u32() } #[no_mangle] @@ -1982,12 +1827,9 @@ pub unsafe extern "C" fn dc_send_locations_to_chat( eprintln!("ignoring careless call to dc_send_locations_to_chat()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - location::send_locations_to_chat(ctx, ChatId::new(chat_id), seconds as i64) - }) - .ok(); + let ctx = &*context; + + block_on({ location::send_locations_to_chat(&ctx, ChatId::new(chat_id), seconds as i64) }); } #[no_mangle] @@ -1999,12 +1841,12 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat( eprintln!("ignoring careless call to dc_is_sending_locations_to_chat()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - location::is_sending_locations_to_chat(ctx, ChatId::new(chat_id)) as libc::c_int - }) - .unwrap_or(0) + let ctx = &*context; + + block_on(location::is_sending_locations_to_chat( + &ctx, + ChatId::new(chat_id), + )) as libc::c_int } #[no_mangle] @@ -2018,10 +1860,9 @@ pub unsafe extern "C" fn dc_set_location( eprintln!("ignoring careless call to dc_set_location()"); return 0; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| location::set(ctx, latitude, longitude, accuracy)) - .unwrap_or(false) as _ + let ctx = &*context; + + block_on(location::set(&ctx, latitude, longitude, accuracy)) as _ } #[no_mangle] @@ -2036,19 +1877,19 @@ pub unsafe extern "C" fn dc_get_locations( eprintln!("ignoring careless call to dc_get_locations()"); return ptr::null_mut(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let res = location::get_range( - ctx, - ChatId::new(chat_id), - contact_id, - timestamp_begin as i64, - timestamp_end as i64, - ); - Box::into_raw(Box::new(dc_array_t::from(res))) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*context; + + block_on(async move { + let res = location::get_range( + &ctx, + ChatId::new(chat_id), + contact_id, + timestamp_begin as i64, + timestamp_end as i64, + ) + .await; + Box::into_raw(Box::new(dc_array_t::from(res))) + }) } #[no_mangle] @@ -2057,12 +1898,14 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) { eprintln!("ignoring careless call to dc_delete_all_locations()"); return; } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - location::delete_all(ctx).log_err(ffi_context, "Failed to delete locations") - }) - .ok(); + let ctx = &*context; + + block_on(async move { + location::delete_all(&ctx) + .await + .log_err(ctx, "Failed to delete locations") + .ok() + }); } // dc_array_t @@ -2329,13 +2172,15 @@ pub unsafe extern "C" fn dc_chatlist_get_summary( Some(&ffi_chat.chat) }; let ffi_list = &*chatlist; - let ffi_context: &ContextWrapper = &*ffi_list.context; - ffi_context - .with_inner(|ctx| { - let lot = ffi_list.list.get_summary(ctx, index as usize, maybe_chat); - Box::into_raw(Box::new(lot)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*ffi_list.context; + + block_on(async move { + let lot = ffi_list + .list + .get_summary(&ctx, index as usize, maybe_chat) + .await; + Box::into_raw(Box::new(lot)) + }) } #[no_mangle] @@ -2414,13 +2259,14 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut return ptr::null_mut(); // NULL explicitly defined as "no image" } let ffi_chat = &*chat; - let ffi_context = &*ffi_chat.context; - ffi_context - .with_inner(|ctx| match ffi_chat.chat.get_profile_image(ctx) { + let ctx = &*ffi_chat.context; + + block_on(async move { + match ffi_chat.chat.get_profile_image(&ctx).await { Some(p) => p.to_string_lossy().strdup(), None => ptr::null_mut(), - }) - .unwrap_or_else(|_| ptr::null_mut()) + } + }) } #[no_mangle] @@ -2430,10 +2276,9 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 { return 0; } let ffi_chat = &*chat; - let ffi_context = &*ffi_chat.context; - ffi_context - .with_inner(|ctx| ffi_chat.chat.get_color(ctx)) - .unwrap_or(0) + let ctx = &*ffi_chat.context; + + block_on(ffi_chat.chat.get_color(&ctx)) } #[no_mangle] @@ -2550,31 +2395,30 @@ pub unsafe extern "C" fn dc_chat_get_info_json( eprintln!("ignoring careless call to dc_chat_get_info_json()"); return "".strdup(); } - let ffi_context = &*context; - ffi_context - .with_inner(|ctx| { - let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)) { - Ok(chat) => chat, - Err(err) => { - error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err); - return "".strdup(); - } - }; - let info = match chat.get_info(ctx) { - Ok(info) => info, - Err(err) => { - error!( - ctx, - "dc_get_chat_info_json() failed to get chat info: {}", err - ); - return "".strdup(); - } - }; - serde_json::to_string(&info) - .unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json") - .strdup() - }) - .unwrap_or_else(|_| "".strdup()) + let ctx = &*context; + + block_on(async move { + let chat = match chat::Chat::load_from_db(&ctx, ChatId::new(chat_id)).await { + Ok(chat) => chat, + Err(err) => { + error!(&ctx, "dc_get_chat_info_json() failed to load chat: {}", err); + return "".strdup(); + } + }; + let info = match chat.get_info(&ctx).await { + Ok(info) => info, + Err(err) => { + error!( + &ctx, + "dc_get_chat_info_json() failed to get chat info: {}", err + ); + return "".strdup(); + } + }; + serde_json::to_string(&info) + .unwrap_or_log_default(&ctx, "dc_get_chat_info_json() failed to serialise to json") + .strdup() + }) } // dc_msg_t @@ -2723,16 +2567,12 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha return "".strdup(); } let ffi_msg = &*msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| { - ffi_msg - .message - .get_file(ctx) - .map(|p| p.strdup()) - .unwrap_or_else(|| "".strdup()) - }) - .unwrap_or_else(|_| "".strdup()) + let ctx = &*ffi_msg.context; + ffi_msg + .message + .get_file(ctx) + .map(|p| p.to_string_lossy().strdup()) + .unwrap_or_else(|| "".strdup()) } #[no_mangle] @@ -2766,10 +2606,9 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 { return 0; } let ffi_msg = &*msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| ffi_msg.message.get_filebytes(ctx)) - .unwrap_or(0) + let ctx = &*ffi_msg.context; + + block_on(ffi_msg.message.get_filebytes(&ctx)) } #[no_mangle] @@ -2828,13 +2667,12 @@ pub unsafe extern "C" fn dc_msg_get_summary( Some(&ffi_chat.chat) }; let ffi_msg = &mut *msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| { - let lot = ffi_msg.message.get_summary(ctx, maybe_chat); - Box::into_raw(Box::new(lot)) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*ffi_msg.context; + + block_on(async move { + let lot = ffi_msg.message.get_summary(&ctx, maybe_chat).await; + Box::into_raw(Box::new(lot)) + }) } #[no_mangle] @@ -2847,15 +2685,14 @@ pub unsafe extern "C" fn dc_msg_get_summarytext( return "".strdup(); } let ffi_msg = &mut *msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| { - ffi_msg - .message - .get_summarytext(ctx, approx_characters.try_into().unwrap_or_default()) - }) - .unwrap_or_default() - .strdup() + let ctx = &*ffi_msg.context; + + block_on({ + ffi_msg + .message + .get_summarytext(&ctx, approx_characters.try_into().unwrap_or_default()) + }) + .strdup() } #[no_mangle] @@ -2945,9 +2782,9 @@ pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut l return "".strdup(); } let ffi_msg = &*msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| ffi_msg.message.get_setupcodebegin(ctx).unwrap_or_default()) + let ctx = &*ffi_msg.context; + + block_on(ffi_msg.message.get_setupcodebegin(&ctx)) .unwrap_or_default() .strdup() } @@ -3029,14 +2866,13 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize( return; } let ffi_msg = &mut *msg; - let ffi_context = &*ffi_msg.context; - ffi_context - .with_inner(|ctx| { - ffi_msg - .message - .latefiling_mediasize(ctx, width, height, duration) - }) - .ok(); + let ctx = &*ffi_msg.context; + + block_on({ + ffi_msg + .message + .latefiling_mediasize(&ctx, width, height, duration) + }); } // dc_contact_t @@ -3140,16 +2976,16 @@ pub unsafe extern "C" fn dc_contact_get_profile_image( return ptr::null_mut(); // NULL explicitly defined as "no profile image" } let ffi_contact = &*contact; - let ffi_context = &*ffi_contact.context; - ffi_context - .with_inner(|ctx| { - ffi_contact - .contact - .get_profile_image(ctx) - .map(|p| p.to_string_lossy().strdup()) - .unwrap_or_else(std::ptr::null_mut) - }) - .unwrap_or_else(|_| ptr::null_mut()) + let ctx = &*ffi_contact.context; + + block_on(async move { + ffi_contact + .contact + .get_profile_image(&ctx) + .await + .map(|p| p.to_string_lossy().strdup()) + .unwrap_or_else(std::ptr::null_mut) + }) } #[no_mangle] @@ -3179,10 +3015,9 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l return 0; } let ffi_contact = &*contact; - let ffi_context = &*ffi_contact.context; - ffi_context - .with_inner(|ctx| ffi_contact.contact.is_verified(ctx) as libc::c_int) - .unwrap_or(0) + let ctx = &*ffi_contact.context; + + block_on(async move { ffi_contact.contact.is_verified(&ctx).await as libc::c_int }) } // dc_lot_t @@ -3280,7 +3115,7 @@ trait ResultExt { /// /// You can do this as soon as the wrapper exists, it does not /// have to be open (which is required for the `warn!()` macro). - fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result; + fn log_err(self, wrapper: &Context, message: &str) -> Result; } impl ResultExt for Result { @@ -3294,11 +3129,9 @@ impl ResultExt for Result { } } - fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result { + fn log_err(self, ctx: &Context, message: &str) -> Result { self.map_err(|err| { - unsafe { - wrapper.warning(&format!("{}: {}", message, err)); - } + warn!(ctx, "{}: {}", message, err); err }) } diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index d3842f42f..619988837 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -1,7 +1,7 @@ -use std::path::Path; use std::str::FromStr; use anyhow::{bail, ensure}; +use async_std::path::Path; use deltachat::chat::{self, Chat, ChatId, ChatVisibility}; use deltachat::chatlist::*; use deltachat::constants::*; @@ -11,7 +11,6 @@ use deltachat::dc_receive_imf::*; use deltachat::dc_tools::*; use deltachat::error::Error; use deltachat::imex::*; -use deltachat::job::*; use deltachat::location; use deltachat::lot::LotState; use deltachat::message::{self, Message, MessageState, MsgId}; @@ -24,78 +23,79 @@ use deltachat::{config, provider}; /// Reset database tables. /// Argument is a bitmask, executing single or multiple actions in one call. /// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4. -fn dc_reset_tables(context: &Context, bits: i32) -> i32 { +async fn reset_tables(context: &Context, bits: i32) { println!("Resetting tables ({})...", bits); if 0 != bits & 1 { - sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap(); + context + .sql() + .execute("DELETE FROM jobs;", paramsv![]) + .await + .unwrap(); println!("(1) Jobs reset."); } if 0 != bits & 2 { - sql::execute( - context, - &context.sql, - "DELETE FROM acpeerstates;", - params![], - ) - .unwrap(); + context + .sql() + .execute("DELETE FROM acpeerstates;", paramsv![]) + .await + .unwrap(); println!("(2) Peerstates reset."); } if 0 != bits & 4 { - sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap(); + context + .sql() + .execute("DELETE FROM keypairs;", paramsv![]) + .await + .unwrap(); println!("(4) Private keypairs reset."); } if 0 != bits & 8 { - sql::execute( - context, - &context.sql, - "DELETE FROM contacts WHERE id>9;", - params![], - ) - .unwrap(); - sql::execute( - context, - &context.sql, - "DELETE FROM chats WHERE id>9;", - params![], - ) - .unwrap(); - sql::execute( - context, - &context.sql, - "DELETE FROM chats_contacts;", - params![], - ) - .unwrap(); - sql::execute( - context, - &context.sql, - "DELETE FROM msgs WHERE id>9;", - params![], - ) - .unwrap(); - sql::execute( - context, - &context.sql, - "DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';", - params![], - ) - .unwrap(); - sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap(); + context + .sql() + .execute("DELETE FROM contacts WHERE id>9;", paramsv![]) + .await + .unwrap(); + context + .sql() + .execute("DELETE FROM chats WHERE id>9;", paramsv![]) + .await + .unwrap(); + context + .sql() + .execute("DELETE FROM chats_contacts;", paramsv![]) + .await + .unwrap(); + context + .sql() + .execute("DELETE FROM msgs WHERE id>9;", paramsv![]) + .await + .unwrap(); + context + .sql() + .execute( + "DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';", + paramsv![], + ) + .await + .unwrap(); + context + .sql() + .execute("DELETE FROM leftgrps;", paramsv![]) + .await + .unwrap(); println!("(8) Rest but server config reset."); } - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); - - 1 } -fn dc_poke_eml_file(context: &Context, filename: impl AsRef) -> Result<(), anyhow::Error> { - let data = dc_read_file(context, filename)?; +async fn poke_eml_file(context: &Context, filename: impl AsRef) -> Result<(), anyhow::Error> { + let data = dc_read_file(context, filename).await?; - if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) { + if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await { println!("dc_receive_imf errored: {:?}", err); } Ok(()) @@ -104,38 +104,29 @@ fn dc_poke_eml_file(context: &Context, filename: impl AsRef) -> Result<(), /// Import a file to the database. /// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on. /// For normal importing, use imex(). -/// -/// @private @memberof Context -/// @param context The context as created by dc_context_new(). -/// @param spec The file or directory to import. NULL for the last command. -/// @return 1=success, 0=error. -fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int { - if !context.sql.is_open() { - error!(context, "Import: Database not opened."); - return 0; - } - - let mut read_cnt = 0; +async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { + let mut read_cnt: usize = 0; let real_spec: String; - /* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */ + // if `spec` is given, remember it for later usage; if it is not given, try to use the last one if let Some(spec) = spec { real_spec = spec.to_string(); context - .sql + .sql() .set_raw_config(context, "import_spec", Some(&real_spec)) + .await .unwrap(); } else { - let rs = context.sql.get_raw_config(context, "import_spec"); + let rs = context.sql().get_raw_config(context, "import_spec").await; if rs.is_none() { error!(context, "Import: No file or folder given."); - return 0; + return false; } real_spec = rs.unwrap(); } if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) { - if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() { + if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() { read_cnt += 1 } } else { @@ -144,7 +135,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int { let dir = std::fs::read_dir(dir_name); if dir.is_err() { error!(context, "Import: Cannot open directory \"{}\".", &real_spec,); - return 0; + return false; } else { let dir = dir.unwrap(); for entry in dir { @@ -157,7 +148,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int { if name.ends_with(".eml") { let path_plus_name = format!("{}/{}", &real_spec, name); println!("Import: {}", path_plus_name); - if dc_poke_eml_file(context, path_plus_name).is_ok() { + if poke_eml_file(context, path_plus_name).await.is_ok() { read_cnt += 1 } } @@ -166,16 +157,19 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int { } println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); if read_cnt > 0 { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); } - 1 + true } -fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { - let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact"); +async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { + let contact = Contact::get_by_id(context, msg.get_from_id()) + .await + .expect("invalid contact"); + let contact_name = contact.get_name(); let contact_id = contact.get_id(); @@ -218,7 +212,7 @@ fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { ); } -fn log_msglist(context: &Context, msglist: &Vec) -> Result<(), Error> { +async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> { let mut lines_out = 0; for &msg_id in msglist { if msg_id.is_daymarker() { @@ -234,8 +228,8 @@ fn log_msglist(context: &Context, msglist: &Vec) -> Result<(), Error> { ); lines_out += 1 } - let msg = Message::load_from_db(context, msg_id)?; - log_msg(context, "", &msg); + let msg = Message::load_from_db(context, msg_id).await?; + log_msg(context, "", &msg).await; } } if lines_out > 0 { @@ -246,18 +240,18 @@ fn log_msglist(context: &Context, msglist: &Vec) -> Result<(), Error> { Ok(()) } -fn log_contactlist(context: &Context, contacts: &Vec) { - let mut contacts = contacts.clone(); +async fn log_contactlist(context: &Context, contacts: &[u32]) { + let mut contacts = contacts.to_vec(); if !contacts.contains(&1) { contacts.push(1); } for contact_id in contacts { let line; let mut line2 = "".to_string(); - if let Ok(contact) = Contact::get_by_id(context, contact_id) { + if let Ok(contact) = Contact::get_by_id(context, contact_id).await { let name = contact.get_name(); let addr = contact.get_addr(); - let verified_state = contact.is_verified(context); + let verified_state = contact.is_verified(context).await; let verified_str = if VerifiedStatus::Unverified != verified_state { if verified_state == VerifiedStatus::BidirectVerified { " √√" @@ -281,7 +275,7 @@ fn log_contactlist(context: &Context, contacts: &Vec) { "addr unset" } ); - let peerstate = Peerstate::from_addr(context, &context.sql, &addr); + let peerstate = Peerstate::from_addr(context, &addr).await; if peerstate.is_some() && contact_id != 1 as libc::c_uint { line2 = format!( ", prefer-encrypt={}", @@ -298,10 +292,9 @@ fn chat_prefix(chat: &Chat) -> &'static str { chat.typ.into() } -pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { - let chat_id = *context.cmdline_sel_chat_id.read().unwrap(); +pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> { let mut sel_chat = if !chat_id.is_unset() { - Chat::load_from_db(context, chat_id).ok() + Chat::load_from_db(&context, *chat_id).await.ok() } else { None }; @@ -342,7 +335,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { configure\n\ connect\n\ disconnect\n\ - interrupt\n\ maybenetwork\n\ housekeeping\n\ help imex (Import/Export)\n\ @@ -405,7 +397,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { =============================================" ), }, - "initiate-key-transfer" => match initiate_key_transfer(context) { + "initiate-key-transfer" => match initiate_key_transfer(&context).await { Ok(setup_code) => println!( "Setup code for the transferred setup message: {}", setup_code, @@ -415,9 +407,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { "get-setupcodebegin" => { ensure!(!arg1.is_empty(), "Argument missing."); let msg_id: MsgId = MsgId::new(arg1.parse()?); - let msg = Message::load_from_db(context, msg_id)?; + let msg = Message::load_from_db(&context, msg_id).await?; if msg.is_setupmessage() { - let setupcodebegin = msg.get_setupcodebegin(context); + let setupcodebegin = msg.get_setupcodebegin(&context).await; println!( "The setup code for setup message {} starts with: {}", msg_id, @@ -432,29 +424,29 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { !arg1.is_empty() && !arg2.is_empty(), "Arguments expected" ); - continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?; + continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?; } "has-backup" => { - has_backup(context, blobdir)?; + has_backup(&context, blobdir).await?; } "export-backup" => { - imex(context, ImexMode::ExportBackup, Some(blobdir)); + imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?; } "import-backup" => { ensure!(!arg1.is_empty(), "Argument missing."); - imex(context, ImexMode::ImportBackup, Some(arg1)); + imex(&context, ImexMode::ImportBackup, Some(arg1)).await?; } "export-keys" => { - imex(context, ImexMode::ExportSelfKeys, Some(blobdir)); + imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?; } "import-keys" => { - imex(context, ImexMode::ImportSelfKeys, Some(blobdir)); + imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?; } "export-setup" => { - let setup_code = create_setup_code(context); + let setup_code = create_setup_code(&context); let file_name = blobdir.join("autocrypt-setup-message.html"); - let file_content = render_setup_file(context, &setup_code)?; - std::fs::write(&file_name, file_content)?; + let file_content = render_setup_file(&context, &setup_code).await?; + async_std::fs::write(&file_name, file_content).await?; println!( "Setup message written to: {}\nSetup code: {}", file_name.display(), @@ -462,49 +454,47 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ); } "poke" => { - ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed"); + ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed"); } "reset" => { ensure!(!arg1.is_empty(), "Argument missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config"); let bits: i32 = arg1.parse()?; ensure!(bits < 16, " must be lower than 16."); - ensure!(0 != dc_reset_tables(context, bits), "Reset failed"); + reset_tables(&context, bits).await; } "stop" => { - context.stop_ongoing(); + context.stop_ongoing().await; } "set" => { ensure!(!arg1.is_empty(), "Argument missing."); let key = config::Config::from_str(&arg1)?; let value = if arg2.is_empty() { None } else { Some(arg2) }; - context.set_config(key, value)?; + context.set_config(key, value).await?; } "get" => { ensure!(!arg1.is_empty(), "Argument missing."); let key = config::Config::from_str(&arg1)?; - let val = context.get_config(key); + let val = context.get_config(key).await; println!("{}={:?}", key, val); } "info" => { - println!("{:#?}", context.get_info()); - } - "interrupt" => { - interrupt_inbox_idle(context); + println!("{:#?}", context.get_info().await); } "maybenetwork" => { - maybe_network(context); + context.maybe_network().await; } "housekeeping" => { - sql::housekeeping(context); + sql::housekeeping(&context).await; } "listchats" | "listarchived" | "chats" => { let listflags = if arg0 == "listarchived" { 0x01 } else { 0 }; let chatlist = Chatlist::try_load( - context, + &context, listflags, if arg1.is_empty() { None } else { Some(arg1) }, None, - )?; + ) + .await?; let cnt = chatlist.len(); if cnt > 0 { @@ -513,20 +503,20 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ); for i in (0..cnt).rev() { - let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; + let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?; println!( "{}#{}: {} [{} fresh] {}", chat_prefix(&chat), chat.get_id(), chat.get_name(), - chat.get_id().get_fresh_msg_cnt(context), + chat.get_id().get_fresh_msg_cnt(&context).await, match chat.visibility { ChatVisibility::Normal => "", ChatVisibility::Archived => "📦", ChatVisibility::Pinned => "📌", }, ); - let lot = chatlist.get_summary(context, i, Some(&chat)); + let lot = chatlist.get_summary(&context, i, Some(&chat)).await; let statestr = if chat.visibility == ChatVisibility::Archived { " [Archived]" } else { @@ -559,7 +549,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ); } } - if location::is_sending_locations_to_chat(context, ChatId::new(0)) { + if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await { println!("Location streaming enabled."); } println!("{} chats", cnt); @@ -569,21 +559,21 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { bail!("Argument [chat-id] is missing."); } if !arg1.is_empty() { - let chat_id = ChatId::new(arg1.parse()?); - println!("Selecting chat {}", chat_id); - sel_chat = Some(Chat::load_from_db(context, chat_id)?); - *context.cmdline_sel_chat_id.write().unwrap() = chat_id; + let id = ChatId::new(arg1.parse()?); + println!("Selecting chat {}", id); + sel_chat = Some(Chat::load_from_db(&context, id).await?); + *chat_id = id; } ensure!(sel_chat.is_some(), "Failed to select chat"); let sel_chat = sel_chat.as_ref().unwrap(); - let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None); - let members = chat::get_chat_contacts(context, sel_chat.id); + let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await; + let members = chat::get_chat_contacts(&context, sel_chat.id).await; let subtitle = if sel_chat.is_device_talk() { "device-talk".to_string() } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { - let contact = Contact::get_by_id(context, members[0])?; + let contact = Contact::get_by_id(&context, members[0]).await?; contact.get_addr().to_string() } else { format!("{} member(s)", members.len()) @@ -599,7 +589,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { } else { "" }, - match sel_chat.get_profile_image(context) { + match sel_chat.get_profile_image(&context).await { Some(icon) => match icon.to_str() { Some(icon) => format!(" Icon: {}", icon), _ => " Icon: Err".to_string(), @@ -607,38 +597,42 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { _ => "".to_string(), }, ); - log_msglist(context, &msglist)?; - if let Some(draft) = sel_chat.get_id().get_draft(context)? { - log_msg(context, "Draft", &draft); + log_msglist(&context, &msglist).await?; + if let Some(draft) = sel_chat.get_id().get_draft(&context).await? { + log_msg(&context, "Draft", &draft).await; } - println!("{} messages.", sel_chat.get_id().get_msg_cnt(context)); - chat::marknoticed_chat(context, sel_chat.get_id())?; + println!( + "{} messages.", + sel_chat.get_id().get_msg_cnt(&context).await + ); + chat::marknoticed_chat(&context, sel_chat.get_id()).await?; } "createchat" => { ensure!(!arg1.is_empty(), "Argument missing."); let contact_id: libc::c_int = arg1.parse()?; - let chat_id = chat::create_by_contact_id(context, contact_id as u32)?; + let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?; println!("Single#{} created successfully.", chat_id,); } "createchatbymsg" => { ensure!(!arg1.is_empty(), "Argument missing"); let msg_id = MsgId::new(arg1.parse()?); - let chat_id = chat::create_by_msg_id(context, msg_id)?; - let chat = Chat::load_from_db(context, chat_id)?; + let chat_id = chat::create_by_msg_id(&context, msg_id).await?; + let chat = Chat::load_from_db(&context, chat_id).await?; println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,); } "creategroup" => { ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = chat::create_group_chat(context, VerifiedStatus::Unverified, arg1)?; + let chat_id = + chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?; println!("Group#{} created successfully.", chat_id); } "createverified" => { ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = chat::create_group_chat(context, VerifiedStatus::Verified, arg1)?; + let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?; println!("VerifiedGroup#{} created successfully.", chat_id); } @@ -648,10 +642,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { let contact_id_0: libc::c_int = arg1.parse()?; if chat::add_contact_to_chat( - context, + &context, sel_chat.as_ref().unwrap().get_id(), contact_id_0 as u32, - ) { + ) + .await + { println!("Contact added to chat."); } else { bail!("Cannot add contact to chat."); @@ -662,17 +658,18 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ensure!(!arg1.is_empty(), "Argument missing."); let contact_id_1: libc::c_int = arg1.parse()?; chat::remove_contact_from_chat( - context, + &context, sel_chat.as_ref().unwrap().get_id(), contact_id_1 as u32, - )?; + ) + .await?; println!("Contact added to chat."); } "groupname" => { ensure!(sel_chat.is_some(), "No chat selected."); ensure!(!arg1.is_empty(), "Argument missing."); - chat::set_chat_name(context, sel_chat.as_ref().unwrap().get_id(), arg1)?; + chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?; println!("Chat name set"); } @@ -680,24 +677,27 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ensure!(sel_chat.is_some(), "No chat selected."); ensure!(!arg1.is_empty(), "Argument missing."); - chat::set_chat_profile_image(context, sel_chat.as_ref().unwrap().get_id(), arg1)?; + chat::set_chat_profile_image(&context, sel_chat.as_ref().unwrap().get_id(), arg1) + .await?; println!("Chat image set"); } "chatinfo" => { ensure!(sel_chat.is_some(), "No chat selected."); - let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id()); + let contacts = + chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await; println!("Memberlist:"); - log_contactlist(context, &contacts); + log_contactlist(&context, &contacts).await; println!( "{} contacts\nLocation streaming: {}", contacts.len(), location::is_sending_locations_to_chat( - context, + &context, sel_chat.as_ref().unwrap().get_id() - ), + ) + .await, ); } "getlocations" => { @@ -705,12 +705,13 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { let contact_id = arg1.parse().unwrap_or_default(); let locations = location::get_range( - context, + &context, sel_chat.as_ref().unwrap().get_id(), contact_id, 0, 0, - ); + ) + .await; let default_marker = "-".to_string(); for location in &locations { let marker = location.marker.as_ref().unwrap_or(&default_marker); @@ -736,7 +737,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ensure!(!arg1.is_empty(), "No timeout given."); let seconds = arg1.parse()?; - location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds); + location::send_locations_to_chat( + &context, + sel_chat.as_ref().unwrap().get_id(), + seconds, + ) + .await; println!( "Locations will be sent to Chat#{} for {} seconds. Use 'setlocation ' to play around.", sel_chat.as_ref().unwrap().get_id(), @@ -751,7 +757,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { let latitude = arg1.parse()?; let longitude = arg2.parse()?; - let continue_streaming = location::set(context, latitude, longitude, 0.); + let continue_streaming = location::set(&context, latitude, longitude, 0.).await; if continue_streaming { println!("Success, streaming should be continued."); } else { @@ -759,7 +765,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { } } "dellocations" => { - location::delete_all(context)?; + location::delete_all(&context).await?; } "send" => { ensure!(sel_chat.is_some(), "No chat selected."); @@ -767,11 +773,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { let msg = format!("{} {}", arg1, arg2); - chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), msg)?; + chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?; } "sendempty" => { ensure!(sel_chat.is_some(), "No chat selected."); - chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), "".into())?; + chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?; } "sendimage" | "sendfile" => { ensure!(sel_chat.is_some(), "No chat selected."); @@ -786,7 +792,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { if !arg2.is_empty() { msg.set_text(Some(arg2.to_string())); } - chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?; + chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; } "listmsgs" => { ensure!(!arg1.is_empty(), "Argument missing."); @@ -797,9 +803,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ChatId::new(0) }; - let msglist = context.search_msgs(chat, arg1); + let msglist = context.search_msgs(chat, arg1).await; - log_msglist(context, &msglist)?; + log_msglist(&context, &msglist).await?; println!("{} messages.", msglist.len()); } "draft" => { @@ -812,10 +818,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { .as_ref() .unwrap() .get_id() - .set_draft(context, Some(&mut draft)); + .set_draft(&context, Some(&mut draft)) + .await; println!("Draft saved."); } else { - sel_chat.as_ref().unwrap().get_id().set_draft(context, None); + sel_chat + .as_ref() + .unwrap() + .get_id() + .set_draft(&context, None) + .await; println!("Draft deleted."); } } @@ -826,21 +838,22 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { ); let mut msg = Message::new(Viewtype::Text); msg.set_text(Some(arg1.to_string())); - chat::add_device_msg(context, None, Some(&mut msg))?; + chat::add_device_msg(&context, None, Some(&mut msg)).await?; } "updatedevicechats" => { - context.update_device_chats()?; + context.update_device_chats().await?; } "listmedia" => { ensure!(sel_chat.is_some(), "No chat selected."); let images = chat::get_chat_media( - context, + &context, sel_chat.as_ref().unwrap().get_id(), Viewtype::Image, Viewtype::Gif, Viewtype::Video, - ); + ) + .await; println!("{} images or videos: ", images.len()); for (i, data) in images.iter().enumerate() { if 0 == i { @@ -849,36 +862,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { print!(", {}", data); } } - print!("\n"); + println!(); } "archive" | "unarchive" | "pin" | "unpin" => { ensure!(!arg1.is_empty(), "Argument missing."); let chat_id = ChatId::new(arg1.parse()?); - chat_id.set_visibility( - context, - match arg0 { - "archive" => ChatVisibility::Archived, - "unarchive" | "unpin" => ChatVisibility::Normal, - "pin" => ChatVisibility::Pinned, - _ => panic!("Unexpected command (This should never happen)"), - }, - )?; + chat_id + .set_visibility( + &context, + match arg0 { + "archive" => ChatVisibility::Archived, + "unarchive" | "unpin" => ChatVisibility::Normal, + "pin" => ChatVisibility::Pinned, + _ => panic!("Unexpected command (This should never happen)"), + }, + ) + .await?; } "delchat" => { ensure!(!arg1.is_empty(), "Argument missing."); let chat_id = ChatId::new(arg1.parse()?); - chat_id.delete(context)?; + chat_id.delete(&context).await?; } "msginfo" => { ensure!(!arg1.is_empty(), "Argument missing."); let id = MsgId::new(arg1.parse()?); - let res = message::get_msg_info(context, id); + let res = message::get_msg_info(&context, id).await; println!("{}", res); } "listfresh" => { - let msglist = context.get_fresh_msgs(); + let msglist = context.get_fresh_msgs().await; - log_msglist(context, &msglist)?; + log_msglist(&context, &msglist).await?; print!("{} fresh messages.", msglist.len()); } "forward" => { @@ -890,37 +905,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { let mut msg_ids = [MsgId::new(0); 1]; let chat_id = ChatId::new(arg2.parse()?); msg_ids[0] = MsgId::new(arg1.parse()?); - chat::forward_msgs(context, &msg_ids, chat_id)?; + chat::forward_msgs(&context, &msg_ids, chat_id).await?; } "markseen" => { ensure!(!arg1.is_empty(), "Argument missing."); - let mut msg_ids = [MsgId::new(0); 1]; + let mut msg_ids = vec![MsgId::new(0)]; msg_ids[0] = MsgId::new(arg1.parse()?); - message::markseen_msgs(context, &msg_ids); + message::markseen_msgs(&context, msg_ids).await; } "star" | "unstar" => { ensure!(!arg1.is_empty(), "Argument missing."); - let mut msg_ids = [MsgId::new(0); 1]; + let mut msg_ids = vec![MsgId::new(0); 1]; msg_ids[0] = MsgId::new(arg1.parse()?); - message::star_msgs(context, &msg_ids, arg0 == "star"); + message::star_msgs(&context, msg_ids, arg0 == "star").await; } "delmsg" => { ensure!(!arg1.is_empty(), "Argument missing."); let mut ids = [MsgId::new(0); 1]; ids[0] = MsgId::new(arg1.parse()?); - message::delete_msgs(context, &ids); + message::delete_msgs(&context, &ids).await; } "listcontacts" | "contacts" | "listverified" => { let contacts = Contact::get_all( - context, + &context, if arg0 == "listverified" { 0x1 | 0x2 } else { 0x2 }, Some(arg1), - )?; - log_contactlist(context, &contacts); + ) + .await?; + log_contactlist(&context, &contacts).await; println!("{} contacts.", contacts.len()); } "addcontact" => { @@ -928,30 +944,30 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { if !arg2.is_empty() { let book = format!("{}\n{}", arg1, arg2); - Contact::add_address_book(context, book)?; + Contact::add_address_book(&context, book).await?; } else { - Contact::create(context, "", arg1)?; + Contact::create(&context, "", arg1).await?; } } "contactinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); let contact_id = arg1.parse()?; - let contact = Contact::get_by_id(context, contact_id)?; + 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) { + match contact.get_profile_image(&context).await { Some(image) => image.to_str().unwrap().to_string(), None => "NoIcon".to_string(), } ); - res += &Contact::get_encrinfo(context, contact_id)?; + res += &Contact::get_encrinfo(&context, contact_id).await?; - let chatlist = Chatlist::try_load(context, 0, None, Some(contact_id))?; + let chatlist = Chatlist::try_load(&context, 0, None, Some(contact_id)).await?; let chatlist_cnt = chatlist.len(); if chatlist_cnt > 0 { res += &format!( @@ -962,7 +978,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { if 0 != i { res += ", "; } - let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; + let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?; res += &format!("{}#{}", chat_prefix(&chat), chat.get_id()); } } @@ -971,11 +987,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { } "delcontact" => { ensure!(!arg1.is_empty(), "Argument missing."); - Contact::delete(context, arg1.parse()?)?; + Contact::delete(&context, arg1.parse()?).await?; } "checkqr" => { ensure!(!arg1.is_empty(), "Argument missing."); - let res = check_qr(context, arg1); + let res = check_qr(&context, arg1).await; println!( "state={}, id={}, text1={:?}, text2={:?}", res.get_state(), @@ -986,7 +1002,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { } "setqr" => { ensure!(!arg1.is_empty(), "Argument missing."); - match set_config_from_qr(context, arg1) { + match set_config_from_qr(&context, arg1).await { Ok(()) => println!("Config set from QR code, you can now call 'configure'"), Err(err) => println!("Cannot set config from QR code: {:?}", err), } @@ -1014,7 +1030,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { // ensure!(!arg1.is_empty(), "Argument missing."); // let event = arg1.parse()?; // let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?; - // let r = context.call_cb(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t); + // 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, @@ -1023,7 +1039,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { "fileinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - if let Ok(buf) = dc_read_file(context, &arg1) { + if let Ok(buf) = dc_read_file(&context, &arg1).await { let (width, height) = dc_get_filemeta(&buf)?; println!("width={}, height={}", width, height); } else { @@ -1033,8 +1049,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { "estimatedeletion" => { ensure!(!arg1.is_empty(), "Argument missing"); let seconds = arg1.parse()?; - let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?; - let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?; + let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?; + let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?; println!( "estimated count of messages older than {} seconds:\non device: {}\non server: {}", seconds, device_cnt, server_cnt @@ -1043,7 +1059,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { "emptyserver" => { ensure!(!arg1.is_empty(), "Argument missing"); - message::dc_empty_server(context, arg1.parse()?); + message::dc_empty_server(&context, arg1.parse()?).await; } "" => (), _ => bail!("Unknown command: \"{}\" type ? for help.", arg0), diff --git a/examples/repl/main.rs b/examples/repl/main.rs index c3fa14569..e53bed1c2 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -6,26 +6,21 @@ #[macro_use] extern crate deltachat; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate rusqlite; use std::borrow::Cow::{self, Borrowed, Owned}; use std::io::{self, Write}; -use std::path::Path; use std::process::Command; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; +use ansi_term::Color; use anyhow::{bail, Error}; +use async_std::path::Path; use deltachat::chat::ChatId; use deltachat::config; use deltachat::context::*; -use deltachat::job::*; use deltachat::oauth2::*; use deltachat::securejoin::*; use deltachat::Event; +use log::{error, info, warn}; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; @@ -38,179 +33,83 @@ use rustyline::{ mod cmdline; use self::cmdline::*; -// Event Handler - -fn receive_event(_context: &Context, event: Event) { +/// Event Handler +fn receive_event(event: Event) { + let yellow = Color::Yellow.normal(); match event { Event::Info(msg) => { /* do not show the event as this would fill the screen */ - println!("{}", msg); + info!("{}", msg); } Event::SmtpConnected(msg) => { - println!("[DC_EVENT_SMTP_CONNECTED] {}", msg); + info!("[SMTP_CONNECTED] {}", msg); } Event::ImapConnected(msg) => { - println!("[DC_EVENT_IMAP_CONNECTED] {}", msg); + info!("[IMAP_CONNECTED] {}", msg); } Event::SmtpMessageSent(msg) => { - println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg); + info!("[SMTP_MESSAGE_SENT] {}", msg); } Event::Warning(msg) => { - println!("[Warning] {}", msg); + warn!("{}", msg); } Event::Error(msg) => { - println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg); + error!("{}", msg); } Event::ErrorNetwork(msg) => { - println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg); + error!("[NETWORK] msg={}", msg); } Event::ErrorSelfNotInGroup(msg) => { - println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg); + error!("[SELF_NOT_IN_GROUP] {}", msg); } Event::MsgsChanged { chat_id, msg_id } => { - print!( - "\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m", - chat_id, msg_id, + info!( + "{}", + yellow.paint(format!( + "Received MSGS_CHANGED(chat_id={}, msg_id={})", + chat_id, msg_id, + )) ); } Event::ContactsChanged(_) => { - print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m"); + info!("{}", yellow.paint("Received CONTACTS_CHANGED()")); } Event::LocationChanged(contact) => { - print!( - "\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m", - contact, + info!( + "{}", + yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact)) ); } Event::ConfigureProgress(progress) => { - print!( - "\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m", - progress, + info!( + "{}", + yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress)) ); } Event::ImexProgress(progress) => { - print!( - "\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m", - progress, + info!( + "{}", + yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress)) ); } Event::ImexFileWritten(file) => { - print!( - "\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m", - file.display() + info!( + "{}", + yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display())) ); } Event::ChatModified(chat) => { - print!( - "\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m", - chat + info!( + "{}", + yellow.paint(format!("Received CHAT_MODIFIED({})", chat)) ); } _ => { - print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event); + info!("Received {:?}", event); } } } -// Threads for waiting for messages and for jobs - -lazy_static! { - static ref HANDLE: Arc>> = Arc::new(Mutex::new(None)); - static ref IS_RUNNING: AtomicBool = AtomicBool::new(true); -} - -struct Handle { - handle_imap: Option>, - handle_mvbox: Option>, - handle_sentbox: Option>, - handle_smtp: Option>, -} - -macro_rules! while_running { - ($code:block) => { - if IS_RUNNING.load(Ordering::Relaxed) { - $code - } else { - break; - } - }; -} - -fn start_threads(c: Arc>) { - if HANDLE.clone().lock().unwrap().is_some() { - return; - } - - println!("Starting threads"); - IS_RUNNING.store(true, Ordering::Relaxed); - - let ctx = c.clone(); - let handle_imap = std::thread::spawn(move || loop { - while_running!({ - perform_inbox_jobs(&ctx.read().unwrap()); - perform_inbox_fetch(&ctx.read().unwrap()); - while_running!({ - let context = ctx.read().unwrap(); - perform_inbox_idle(&context); - }); - }); - }); - - let ctx = c.clone(); - let handle_mvbox = std::thread::spawn(move || loop { - while_running!({ - perform_mvbox_fetch(&ctx.read().unwrap()); - while_running!({ - perform_mvbox_idle(&ctx.read().unwrap()); - }); - }); - }); - - let ctx = c.clone(); - let handle_sentbox = std::thread::spawn(move || loop { - while_running!({ - perform_sentbox_fetch(&ctx.read().unwrap()); - while_running!({ - perform_sentbox_idle(&ctx.read().unwrap()); - }); - }); - }); - - let ctx = c; - let handle_smtp = std::thread::spawn(move || loop { - while_running!({ - perform_smtp_jobs(&ctx.read().unwrap()); - while_running!({ - perform_smtp_idle(&ctx.read().unwrap()); - }); - }); - }); - - *HANDLE.clone().lock().unwrap() = Some(Handle { - handle_imap: Some(handle_imap), - handle_mvbox: Some(handle_mvbox), - handle_sentbox: Some(handle_sentbox), - handle_smtp: Some(handle_smtp), - }); -} - -fn stop_threads(context: &Context) { - if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() { - println!("Stopping threads"); - IS_RUNNING.store(false, Ordering::Relaxed); - - interrupt_inbox_idle(context); - interrupt_mvbox_idle(context); - interrupt_sentbox_idle(context); - interrupt_smtp_idle(context); - - handle.handle_imap.take().unwrap().join().unwrap(); - handle.handle_mvbox.take().unwrap().join().unwrap(); - handle.handle_sentbox.take().unwrap().join().unwrap(); - handle.handle_smtp.take().unwrap().join().unwrap(); - } -} - // === The main loop struct DcHelper { @@ -367,69 +266,81 @@ impl Highlighter for DcHelper { impl Helper for DcHelper {} -fn main_0(args: Vec) -> Result<(), Error> { +async fn start(args: Vec) -> Result<(), Error> { if args.len() < 2 { println!("Error: Bad arguments, expected [db-name]."); bail!("No db-name specified"); } - let context = Context::new( - Box::new(receive_event), - "CLI".into(), - Path::new(&args[1]).to_path_buf(), - )?; + let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?; + + let events = context.get_event_emitter(); + async_std::task::spawn(async move { + while let Some(event) = events.recv().await { + receive_event(event); + } + }); println!("Delta Chat Core is awaiting your commands."); - let ctx = Arc::new(RwLock::new(context)); - let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) .build(); - let h = DcHelper { - completer: FilenameCompleter::new(), - highlighter: MatchingBracketHighlighter::new(), - hinter: HistoryHinter {}, - }; - let mut rl = Editor::with_config(config); - rl.set_helper(Some(h)); - rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); - rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); - if rl.load_history(".dc-history.txt").is_err() { - println!("No previous history."); - } + let mut selected_chat = ChatId::default(); + let (reader_s, reader_r) = async_std::sync::channel(100); + let input_loop = async_std::task::spawn_blocking(move || { + let h = DcHelper { + completer: FilenameCompleter::new(), + highlighter: MatchingBracketHighlighter::new(), + hinter: HistoryHinter {}, + }; + let mut rl = Editor::with_config(config); + rl.set_helper(Some(h)); + rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); + rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); + if rl.load_history(".dc-history.txt").is_err() { + println!("No previous history."); + } - loop { - let p = "> "; - let readline = rl.readline(&p); - match readline { - Ok(line) => { - // TODO: ignore "set mail_pw" - rl.add_history_entry(line.as_str()); - let ctx = ctx.clone(); - match handle_cmd(line.trim(), ctx) { - Ok(ExitResult::Continue) => {} - Ok(ExitResult::Exit) => break, - Err(err) => println!("Error: {}", err), + loop { + let p = "> "; + let readline = rl.readline(&p); + + match readline { + Ok(line) => { + // TODO: ignore "set mail_pw" + rl.add_history_entry(line.as_str()); + async_std::task::block_on(reader_s.send(line)); + } + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { + println!("Exiting..."); + drop(reader_s); + break; + } + Err(err) => { + println!("Error: {}", err); + drop(reader_s); + break; } } - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { - println!("Exiting..."); - break; - } - Err(err) => { - println!("Error: {}", err); - break; - } + } + + rl.save_history(".dc-history.txt")?; + println!("history saved"); + Ok::<_, Error>(()) + }); + + while let Ok(line) = reader_r.recv().await { + match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await { + Ok(ExitResult::Continue) => {} + Ok(ExitResult::Exit) => break, + Err(err) => println!("Error: {}", err), } } - rl.save_history(".dc-history.txt")?; - println!("history saved"); - { - stop_threads(&ctx.read().unwrap()); - } + context.stop_io().await; + input_loop.await?; Ok(()) } @@ -440,43 +351,29 @@ enum ExitResult { Exit, } -fn handle_cmd(line: &str, ctx: Arc>) -> Result { +async fn handle_cmd( + line: &str, + ctx: Context, + selected_chat: &mut ChatId, +) -> Result { let mut args = line.splitn(2, ' '); let arg0 = args.next().unwrap_or_default(); let arg1 = args.next().unwrap_or_default(); match arg0 { "connect" => { - start_threads(ctx); + ctx.start_io().await; } "disconnect" => { - stop_threads(&ctx.read().unwrap()); - } - "smtp-jobs" => { - if HANDLE.clone().lock().unwrap().is_some() { - println!("smtp-jobs are already running in a thread.",); - } else { - perform_smtp_jobs(&ctx.read().unwrap()); - } - } - "imap-jobs" => { - if HANDLE.clone().lock().unwrap().is_some() { - println!("inbox-jobs are already running in a thread."); - } else { - perform_inbox_jobs(&ctx.read().unwrap()); - } + ctx.stop_io().await; } "configure" => { - start_threads(ctx.clone()); - ctx.read().unwrap().configure(); + ctx.configure().await?; } "oauth2" => { - if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) { - let oauth2_url = dc_get_oauth2_url( - &ctx.read().unwrap(), - &addr, - "chat.delta:/com.b44t.messenger", - ); + if 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() { println!("OAuth2 not available for {}.", &addr); } else { @@ -491,11 +388,10 @@ fn handle_cmd(line: &str, ctx: Arc>) -> Result { - start_threads(ctx.clone()); - if let Some(mut qr) = dc_get_securejoin_qr( - &ctx.read().unwrap(), - ChatId::new(arg1.parse().unwrap_or_default()), - ) { + ctx.start_io().await; + if let Some(mut qr) = + dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await + { if !qr.is_empty() { if arg0 == "getbadqr" && qr.len() > 40 { qr.replace_range(12..22, "0000000000") @@ -511,23 +407,23 @@ fn handle_cmd(line: &str, ctx: Arc>) -> Result { - start_threads(ctx.clone()); + ctx.start_io().await; if !arg0.is_empty() { - dc_join_securejoin(&ctx.read().unwrap(), arg1); + dc_join_securejoin(&ctx, arg1).await; } } "exit" | "quit" => return Ok(ExitResult::Exit), - _ => dc_cmdline(&ctx.read().unwrap(), line)?, + _ => cmdline(ctx.clone(), line, selected_chat).await?, } Ok(ExitResult::Continue) } -pub fn main() -> Result<(), Error> { +fn main() -> Result<(), Error> { let _ = pretty_env_logger::try_init(); - let args: Vec = std::env::args().collect(); - main_0(args)?; + let args = std::env::args().collect(); + async_std::task::block_on(async move { start(args).await })?; Ok(()) } diff --git a/examples/simple.rs b/examples/simple.rs index 9ab89ed6d..2d37769ad 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,7 +1,3 @@ -extern crate deltachat; - -use std::sync::{Arc, RwLock}; -use std::{thread, time}; use tempfile::tempdir; use deltachat::chat; @@ -9,103 +5,96 @@ use deltachat::chatlist::*; use deltachat::config; use deltachat::contact::*; use deltachat::context::*; -use deltachat::job::{ - perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle, - perform_smtp_jobs, -}; +use deltachat::message::Message; use deltachat::Event; -fn cb(_ctx: &Context, event: Event) { - print!("[{:?}]", event); - +fn cb(event: Event) { match event { Event::ConfigureProgress(progress) => { - println!(" progress: {}", progress); + log::info!("progress: {}", progress); } - Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => { - println!(" {}", msg); + Event::Info(msg) => { + log::info!("{}", msg); } - _ => { - println!(); + Event::Warning(msg) => { + log::warn!("{}", msg); + } + Event::Error(msg) | Event::ErrorNetwork(msg) => { + log::error!("{}", msg); + } + event => { + log::info!("{:?}", event); } } } -fn main() { +/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`. +#[async_std::main] +async fn main() { + pretty_env_logger::try_init_timed().ok(); + let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); - println!("creating database {:?}", dbfile); - let ctx = - Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context"); - let running = Arc::new(RwLock::new(true)); - let info = ctx.get_info(); - let duration = time::Duration::from_millis(4000); - println!("info: {:#?}", info); + log::info!("creating database {:?}", dbfile); + let ctx = Context::new("FakeOs".into(), dbfile.into()) + .await + .expect("Failed to create context"); + let info = ctx.get_info().await; + log::info!("info: {:#?}", info); - let ctx = Arc::new(ctx); - let ctx1 = ctx.clone(); - let r1 = running.clone(); - let t1 = thread::spawn(move || { - while *r1.read().unwrap() { - perform_inbox_jobs(&ctx1); - if *r1.read().unwrap() { - perform_inbox_fetch(&ctx1); - - if *r1.read().unwrap() { - perform_inbox_idle(&ctx1); - } - } + let events = ctx.get_event_emitter(); + let events_spawn = async_std::task::spawn(async move { + while let Some(event) = events.recv().await { + cb(event); } }); - let ctx1 = ctx.clone(); - let r1 = running.clone(); - let t2 = thread::spawn(move || { - while *r1.read().unwrap() { - perform_smtp_jobs(&ctx1); - if *r1.read().unwrap() { - perform_smtp_idle(&ctx1); - } - } - }); - - println!("configuring"); + log::info!("configuring"); let args = std::env::args().collect::>(); - assert_eq!(args.len(), 2, "missing password"); - let pw = args[1].clone(); - ctx.set_config(config::Config::Addr, Some("d@testrun.org")) + assert_eq!(args.len(), 3, "requires email password"); + let email = args[1].clone(); + let pw = args[2].clone(); + ctx.set_config(config::Config::Addr, Some(&email)) + .await + .unwrap(); + ctx.set_config(config::Config::MailPw, Some(&pw)) + .await .unwrap(); - ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap(); - ctx.configure(); - thread::sleep(duration); + ctx.configure().await.unwrap(); - println!("sending a message"); - let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap(); - let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap(); - chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap(); + log::info!("------ RUN ------"); + ctx.start_io().await; + log::info!("--- SENDING A MESSAGE ---"); - println!("fetching chats.."); - let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap(); + let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com") + .await + .unwrap(); + let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap(); - for i in 0..chats.len() { - let summary = chats.get_summary(&ctx, 0, None); - let text1 = summary.get_text1(); - let text2 = summary.get_text2(); - println!("chat: {} - {:?} - {:?}", i, text1, text2,); + for i in 0..1 { + log::info!("sending message {}", i); + chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i)) + .await + .unwrap(); } - thread::sleep(duration); + // wait for the message to be sent out + async_std::task::sleep(std::time::Duration::from_secs(1)).await; - println!("stopping threads"); + log::info!("fetching chats.."); + let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap(); - *running.write().unwrap() = false; - deltachat::job::interrupt_inbox_idle(&ctx); - deltachat::job::interrupt_smtp_idle(&ctx); + for i in 0..chats.len() { + let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap()) + .await + .unwrap(); + log::info!("[{}] msg: {:?}", i, msg); + } - println!("joining"); - t1.join().unwrap(); - t2.join().unwrap(); - - println!("closing"); + log::info!("stopping"); + ctx.stop_io().await; + log::info!("closing"); + drop(ctx); + events_spawn.await; } diff --git a/python/README.rst b/python/README.rst index 077d81ab0..002880dd9 100644 --- a/python/README.rst +++ b/python/README.rst @@ -113,10 +113,10 @@ You may look at `examples `_. .. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust -Building manylinux1 based wheels -================================ +Building manylinux based wheels +==================================== -Building portable manylinux1 wheels which come with libdeltachat.so +Building portable manylinux wheels which come with libdeltachat.so can be done with docker-tooling. using docker pull / premade images diff --git a/python/examples/echo_and_quit.py b/python/examples/echo_and_quit.py index 367d1fd58..0dd3707fe 100644 --- a/python/examples/echo_and_quit.py +++ b/python/examples/echo_and_quit.py @@ -14,8 +14,11 @@ class EchoPlugin: # unconditionally accept the chat message.accept_sender_contact() addr = message.get_sender_contact().addr - text = message.text - message.chat.send_text("echoing from {}:\n{}".format(addr, text)) + if message.is_system_message(): + message.chat.send_text("echoing system message from {}:\n{}".format(addr, message)) + else: + text = message.text + message.chat.send_text("echoing from {}:\n{}".format(addr, text)) @account_hookimpl def ac_message_delivered(self, message): diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py index da32b127d..c9c38c26d 100644 --- a/python/examples/test_examples.py +++ b/python/examples/test_examples.py @@ -3,7 +3,7 @@ import pytest import py import echo_and_quit import group_tracking -from deltachat.eventlogger import FFIEventLogger +from deltachat.events import FFIEventLogger @pytest.fixture(scope='session') @@ -17,16 +17,23 @@ def datadir(): pytest.skip('test-data directory not found') -def test_echo_quit_plugin(acfactory): +def test_echo_quit_plugin(acfactory, lp): + lp.sec("creating one echo_and_quit bot") botproc = acfactory.run_bot_process(echo_and_quit) + lp.sec("creating a temp account to contact the bot") ac1 = acfactory.get_one_online_account() + + lp.sec("sending a message to the bot") bot_contact = ac1.create_contact(botproc.addr) ch1 = ac1.create_chat_by_contact(bot_contact) ch1.send_text("hello") + + lp.sec("waiting for the bot-reply to arrive") reply = ac1._evtracker.wait_next_incoming_message() assert "hello" in reply.text assert reply.chat == ch1 + lp.sec("send quit sequence") ch1.send_text("/quit") botproc.wait() diff --git a/python/fail_test.py b/python/fail_test.py new file mode 100644 index 000000000..04229e660 --- /dev/null +++ b/python/fail_test.py @@ -0,0 +1,7 @@ +from __future__ import print_function +from deltachat import capi +from deltachat.capi import ffi, lib + +if __name__ == "__main__": + ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL) + lib.dc_stop_io(ctx) diff --git a/python/install_python_bindings.py b/python/install_python_bindings.py index 106d7fdbe..bc4fb4cf7 100755 --- a/python/install_python_bindings.py +++ b/python/install_python_bindings.py @@ -19,6 +19,7 @@ if __name__ == "__main__": cmd = ["cargo", "build", "-p", "deltachat_ffi"] if target == 'release': cmd.append("--release") + print("running:", " ".join(cmd)) subprocess.check_call(cmd) subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True) diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index 41c513145..0d8a84ecc 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -1,13 +1,13 @@ import sys -from . import capi, const, hookspec -from .capi import ffi +from . import capi, const, hookspec # noqa +from .capi import ffi # noqa from .account import Account # noqa from .message import Message # noqa from .contact import Contact # noqa from .chat import Chat # noqa from .hookspec import account_hookimpl, global_hookimpl # noqa -from . import eventlogger +from . import events from pkg_resources import get_distribution, DistributionNotFound try: @@ -17,64 +17,6 @@ except DistributionNotFound: __version__ = "0.0.0.dev0-unknown" -_DC_CALLBACK_MAP = {} - - -@capi.ffi.def_extern() -def py_dc_callback(ctx, evt, data1, data2): - """The global event handler. - - CFFI only allows us to set one global event handler, so this one - looks up the correct event handler for the given context. - """ - try: - callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0) - except AttributeError: - # we are in a deep in GC-free/interpreter shutdown land - # nothing much better to do here than: - return 0 - - # the following code relates to the deltachat/_build.py's helper - # function which provides us signature info of an event call - evt_name = get_dc_event_name(evt) - event_sig_types = capi.lib.dc_get_event_signature_types(evt) - if data1 and event_sig_types & 1: - data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8") - if data2 and event_sig_types & 2: - data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8") - try: - if isinstance(data2, bytes): - data2 = data2.decode("utf8") - except UnicodeDecodeError: - # XXX ignoring the decode error is not quite correct but for now - # i don't want to hunt down encoding problems in the c lib - pass - try: - ret = callback(ctx, evt_name, data1, data2) - if ret is None: - ret = 0 - assert isinstance(ret, int), repr(ret) - if event_sig_types & 4: - return ffi.cast('uintptr_t', ret) - elif event_sig_types & 8: - return ffi.cast('int', ret) - except: # noqa - raise - ret = 0 - return ret - - -def set_context_callback(dc_context, func): - _DC_CALLBACK_MAP[dc_context] = func - - -def clear_context_callback(dc_context): - try: - _DC_CALLBACK_MAP.pop(dc_context, None) - except AttributeError: - pass - - def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): if not _DC_EVENTNAME_MAP: for name, val in vars(const).items(): @@ -97,7 +39,7 @@ def unregister_global_plugin(plugin): gm.unregister(plugin) -register_global_plugin(eventlogger) +register_global_plugin(events) def run_cmdline(argv=None, account_plugins=None): @@ -118,9 +60,13 @@ def run_cmdline(argv=None, account_plugins=None): ac = Account(args.db) if args.show_ffi: - log = eventlogger.FFIEventLogger(ac, "bot") + log = events.FFIEventLogger(ac, "bot") ac.add_account_plugin(log) + for plugin in account_plugins or []: + print("adding plugin", plugin) + ac.add_account_plugin(plugin) + if not ac.is_configured(): assert args.email and args.password, ( "you must specify --email and --password once to configure this database/account" @@ -130,12 +76,11 @@ def run_cmdline(argv=None, account_plugins=None): ac.set_config("mvbox_move", "0") ac.set_config("mvbox_watch", "0") ac.set_config("sentbox_watch", "0") - - for plugin in account_plugins or []: - ac.add_account_plugin(plugin) + ac.configure() + ac.wait_configure_finish() # start IO threads and configure if neccessary - ac.start() + ac.start_io() print("{}: waiting for message".format(ac.get_config("addr"))) diff --git a/python/src/deltachat/_build.py b/python/src/deltachat/_build.py index 7e0039fa4..25936755f 100644 --- a/python/src/deltachat/_build.py +++ b/python/src/deltachat/_build.py @@ -45,22 +45,9 @@ def ffibuilder(): 'deltachat.capi', """ #include - const char * dupstring_helper(const char* string) + int dc_event_has_string_data(int e) { - return strdup(string); - } - int dc_get_event_signature_types(int e) - { - int result = 0; - if (DC_EVENT_DATA1_IS_STRING(e)) - result |= 1; - if (DC_EVENT_DATA2_IS_STRING(e)) - result |= 2; - if (DC_EVENT_RETURNS_STRING(e)) - result |= 4; - if (DC_EVENT_RETURNS_INT(e)) - result |= 8; - return result; + return DC_EVENT_DATA2_IS_STRING(e); } """, include_dirs=incs, @@ -71,8 +58,7 @@ def ffibuilder(): builder.cdef(""" typedef int... time_t; void free(void *ptr); - extern const char * dupstring_helper(const char* string); - extern int dc_get_event_signature_types(int); + extern int dc_event_has_string_data(int); """) distutils.log.set_verbosity(distutils.log.INFO) cc = distutils.ccompiler.new_compiler(force=True) @@ -92,13 +78,6 @@ def ffibuilder(): finally: shutil.rmtree(tmpdir) - builder.cdef(""" - extern "Python" uintptr_t py_dc_callback( - dc_context_t* context, - int event, - uintptr_t data1, - uintptr_t data2); - """) return builder diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index d1b4ac519..34f2e63ba 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -1,22 +1,20 @@ """ Account class implementation. """ from __future__ import print_function -import atexit from contextlib import contextmanager from email.utils import parseaddr -import queue from threading import Event import os from array import array -import deltachat from . import const from .capi import ffi, lib from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot from .chat import Chat -from .message import Message, map_system_message +from .message import Message from .contact import Contact -from .tracker import ImexTracker -from . import hookspec, iothreads +from .tracker import ImexTracker, ConfigureTracker +from . import hookspec +from .events import EventThread class MissingCredentials(ValueError): @@ -40,28 +38,24 @@ class Account(object): # initialize per-account plugin system self._pm = hookspec.PerAccount._make_plugin_manager() self._logging = logging + self.add_account_plugin(self) - self._dc_context = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)), - _destroy_dc_context, - ) - - hook = hookspec.Global._get_plugin_manager().hook - - self._threads = iothreads.IOThreads(self) - self._hook_event_queue = queue.Queue() - self._in_use_iter_events = False - self._shutdown_event = Event() - - # open database self.db_path = db_path if hasattr(db_path, "encode"): db_path = db_path.encode("utf8") - if not lib.dc_open(self._dc_context, db_path, ffi.NULL): - raise ValueError("Could not dc_open: {}".format(db_path)) + + self._dc_context = ffi.gc( + lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL), + lib.dc_context_unref, + ) + if self._dc_context == ffi.NULL: + raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path)) + + self._shutdown_event = Event() + self._event_thread = EventThread(self) self._configkeys = self.get_config("sys.config_keys").split() - atexit.register(self.shutdown) + hook = hookspec.Global._get_plugin_manager().hook hook.dc_account_init(account=self) def disable_logging(self): @@ -72,16 +66,10 @@ class Account(object): """ re-enable logging. """ self._logging = True - @hookspec.account_hookimpl - def ac_process_ffi_event(self, ffi_event): - for name, kwargs in self._map_ffi_event(ffi_event): - ev = HookEvent(self, name=name, kwargs=kwargs) - self._hook_event_queue.put(ev) - # def __del__(self): # self.shutdown() - def ac_log_line(self, msg): + def log(self, msg): if self._logging: self._pm.hook.ac_log_line(message=msg) @@ -172,7 +160,7 @@ class Account(object): :returns: True if account is configured. """ - return bool(lib.dc_is_configured(self._dc_context)) + return True if lib.dc_is_configured(self._dc_context) else False def set_avatar(self, img_path): """Set self avatar. @@ -250,7 +238,7 @@ class Account(object): :returns: True if deletion succeeded (contact was deleted) """ contact_id = contact.id - assert contact._dc_context == self._dc_context + assert contact.account == self assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL return bool(lib.dc_delete_contact(self._dc_context, contact_id)) @@ -298,7 +286,7 @@ class Account(object): :returns: a :class:`deltachat.chat.Chat` object. """ if hasattr(contact, "id"): - if contact._dc_context != self._dc_context: + if contact.account != self: raise ValueError("Contact belongs to a different Account") contact_id = contact.id else: @@ -318,7 +306,7 @@ class Account(object): :returns: a :class:`deltachat.chat.Chat` object. """ if hasattr(message, "id"): - if self._dc_context != message._dc_context: + if message.account != self: raise ValueError("Message belongs to a different Account") msg_id = message.id else: @@ -438,8 +426,6 @@ class Account(object): def _export(self, path, imex_cmd): with self.temp_plugin(ImexTracker()) as imex_tracker: lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL) - if not self._threads.is_started(): - lib.dc_perform_imap_jobs(self._dc_context) return imex_tracker.wait_finish() def import_self_keys(self, path): @@ -462,8 +448,6 @@ class Account(object): def _import(self, path, imex_cmd): with self.temp_plugin(ImexTracker()) as imex_tracker: lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL) - if not self._threads.is_started(): - lib.dc_perform_imap_jobs(self._dc_context) imex_tracker.wait_finish() def initiate_key_transfer(self): @@ -472,8 +456,8 @@ class Account(object): If sending out was unsuccessful, a RuntimeError is raised. """ self.check_is_configured() - if not self._threads.is_started(): - raise RuntimeError("threads not running, can not send out") + if not self.is_started(): + raise RuntimeError("IO not running, can not send out") res = lib.dc_initiate_key_transfer(self._dc_context) if res == ffi.NULL: raise RuntimeError("could not send out autocrypt setup message") @@ -555,6 +539,10 @@ class Account(object): self._pm.check_pending() return plugin + def remove_account_plugin(self, plugin, name=None): + """ remove an account plugin. """ + self._pm.unregister(plugin, name=name) + @contextmanager def temp_plugin(self, plugin): """ run a with-block with the given plugin temporarily registered. """ @@ -566,110 +554,82 @@ class Account(object): """ Stop ongoing securejoin, configuration or other core jobs. """ lib.dc_stop_ongoing_process(self._dc_context) - def start(self, callback_thread=True): - """ start this account (activate imap/smtp threads etc.) - and return immediately. + def start_io(self): + """ start this account's IO scheduling (Rust-core async scheduler) - If this account is not configured, an internal configuration - job will be scheduled if config values are sufficiently specified. + If this account is not configured an Exception is raised. + You need to call account.configure() and account.wait_configure_finish() + before. - You may call `wait_shutdown` or `shutdown` after the - account is in started mode. + You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the + account is started. :raises MissingCredentials: if `addr` and `mail_pw` values are not set. + :raises ConfigureFailed: if the account could not be configured. - :returns: None + :returns: None (account is configured and with io-scheduling running) """ if not self.is_configured(): - if not self.get_config("addr") or not self.get_config("mail_pw"): - raise MissingCredentials("addr or mail_pwd not set in config") - lib.dc_configure(self._dc_context) - self._threads.start(callback_thread=callback_thread) + raise ValueError("account not configured, cannot start io") + lib.dc_start_io(self._dc_context) + + def configure(self): + assert not self.is_configured() + assert not hasattr(self, "_configtracker") + if not self.get_config("addr") or not self.get_config("mail_pw"): + raise MissingCredentials("addr or mail_pwd not set in config") + if hasattr(self, "_configtracker"): + self.remove_account_plugin(self._configtracker) + self._configtracker = ConfigureTracker() + self.add_account_plugin(self._configtracker) + lib.dc_configure(self._dc_context) + + def wait_configure_finish(self): + try: + self._configtracker.wait_finish() + finally: + self.remove_account_plugin(self._configtracker) + del self._configtracker + + def is_started(self): + return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context)) def wait_shutdown(self): """ wait until shutdown of this account has completed. """ self._shutdown_event.wait() - def shutdown(self, wait=True): - """ shutdown account, stop threads and close and remove - underlying dc_context and callbacks. """ - dc_context = self._dc_context - if dc_context is None: + def stop_io(self): + """ stop core IO scheduler if it is running. """ + self.log("stop_ongoing") + self.stop_ongoing() + + if bool(lib.dc_is_io_running(self._dc_context)): + self.log("dc_stop_io (stop core IO scheduler)") + lib.dc_stop_io(self._dc_context) + else: + self.log("stop_scheduler called on non-running context") + + def shutdown(self): + """ shutdown and destroy account (stop callback thread, close and remove + underlying dc_context).""" + if self._dc_context is None: return - if self._threads.is_started(): - self.stop_ongoing() - self._threads.stop(wait=False) - lib.dc_close(dc_context) - self._hook_event_queue.put(None) - self._threads.stop(wait=wait) # to wait for threads + self.stop_io() + + self.log("remove dc_context references") + # the dc_context_unref triggers get_next_event to return ffi.NULL + # which in turns makes the event thread finish execution self._dc_context = None - atexit.unregister(self.shutdown) + + self.log("wait for event thread to finish") + self._event_thread.wait() + self._shutdown_event.set() + hook = hookspec.Global._get_plugin_manager().hook - hook.dc_account_after_shutdown(account=self, dc_context=dc_context) - - def _handle_current_events(self): - """ handle all currently queued events and then return. """ - while 1: - try: - event = self._hook_event_queue.get(block=False) - except queue.Empty: - break - else: - event.call_hook() - - def iter_events(self, timeout=None): - """ yield hook events until shutdown. - - It is not allowed to call iter_events() from multiple threads. - """ - if self._in_use_iter_events: - raise RuntimeError("can only call iter_events() from one thread") - self._in_use_iter_events = True - while 1: - event = self._hook_event_queue.get(timeout=timeout) - if event is None: - break - yield event - - def _map_ffi_event(self, ffi_event): - name = ffi_event.name - if name == "DC_EVENT_CONFIGURE_PROGRESS": - data1 = ffi_event.data1 - if data1 == 0 or data1 == 1000: - success = data1 == 1000 - yield "ac_configure_completed", dict(success=success) - elif name == "DC_EVENT_INCOMING_MSG": - msg = self.get_message_by_id(ffi_event.data2) - yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) - elif name == "DC_EVENT_MSGS_CHANGED": - if ffi_event.data2 != 0: - msg = self.get_message_by_id(ffi_event.data2) - if msg.is_outgoing(): - res = map_system_message(msg) - if res and res[0].startswith("ac_member"): - yield res - yield "ac_outgoing_message", dict(message=msg) - elif msg.is_in_fresh(): - yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) - elif name == "DC_EVENT_MSG_DELIVERED": - msg = self.get_message_by_id(ffi_event.data2) - yield "ac_message_delivered", dict(message=msg) - elif name == "DC_EVENT_CHAT_MODIFIED": - chat = self.get_chat_by_id(ffi_event.data1) - yield "ac_chat_modified", dict(chat=chat) - - -def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref): - # destructor for dc_context - dc_context_unref(dc_context) - try: - deltachat.clear_context_callback(dc_context) - except (TypeError, AttributeError): - # we are deep into Python Interpreter shutdown, - # so no need to clear the callback context mapping. - pass + hook.dc_account_after_shutdown(account=self) + self.log("shutdown finished") class ScannedQRCode: @@ -685,17 +645,3 @@ class ScannedQRCode: @property def contact_id(self): return self._dc_lot.id() - - -class HookEvent: - def __init__(self, account, name, kwargs): - assert hasattr(account._pm.hook, name), name - self.account = account - self.name = name - self.kwargs = kwargs - - def call_hook(self): - hook = getattr(self.account._pm.hook, self.name, None) - if hook is None: - raise ValueError("event_name {} unknown".format(self.name)) - return hook(**self.kwargs) diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index f2d6cd09a..21f4da558 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -19,12 +19,11 @@ class Chat(object): def __init__(self, account, id): self.account = account - self._dc_context = account._dc_context self.id = id def __eq__(self, other): return self.id == getattr(other, "id", None) and \ - self._dc_context == getattr(other, "_dc_context", None) + self.account._dc_context == other.account._dc_context def __ne__(self, other): return not (self == other) @@ -35,7 +34,7 @@ class Chat(object): @property def _dc_chat(self): return ffi.gc( - lib.dc_get_chat(self._dc_context, self.id), + lib.dc_get_chat(self.account._dc_context, self.id), lib.dc_chat_unref ) @@ -47,7 +46,7 @@ class Chat(object): - does not delete messages on server - the chat or contact is not blocked, new message will arrive """ - lib.dc_delete_chat(self._dc_context, self.id) + lib.dc_delete_chat(self.account._dc_context, self.id) # ------ chat status/metadata API ------------------------------ @@ -105,7 +104,7 @@ class Chat(object): :returns: None """ name = as_dc_charpointer(name) - return lib.dc_set_chat_name(self._dc_context, self.id, name) + return lib.dc_set_chat_name(self.account._dc_context, self.id, name) def mute(self, duration=None): """ mutes the chat @@ -117,7 +116,7 @@ class Chat(object): mute_duration = -1 else: mute_duration = duration - ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration) + ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration) if not bool(ret): raise ValueError("Call to dc_set_chat_mute_duration failed") @@ -126,7 +125,7 @@ class Chat(object): :returns: None """ - ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0) + ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0) if not bool(ret): raise ValueError("Failed to unmute chat") @@ -152,7 +151,7 @@ class Chat(object): in a second channel (typically used by mobiles with QRcode-show + scan UX) where account.join_with_qrcode(qr) needs to be called. """ - res = lib.dc_get_securejoin_qr(self._dc_context, self.id) + res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id) return from_dc_charpointer(res) # ------ chat messaging API ------------------------------ @@ -174,7 +173,7 @@ class Chat(object): assert msg.id != 0 # get a fresh copy of dc_msg, the core needs it msg = Message.from_db(self.account, msg.id) - sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) + sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg) if sent_id == 0: raise ValueError("message could not be sent") # modify message in place to avoid bad state for the caller @@ -189,7 +188,7 @@ class Chat(object): :returns: the resulting :class:`deltachat.message.Message` instance """ msg = as_dc_charpointer(text) - msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg) + msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg) if msg_id == 0: raise ValueError("message could not be send, does chat exist?") return Message.from_db(self.account, msg_id) @@ -204,7 +203,7 @@ class Chat(object): """ msg = Message.new_empty(self.account, view_type="file") msg.set_file(path, mime_type) - sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) + sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg) if sent_id == 0: raise ValueError("message could not be sent") return Message.from_db(self.account, sent_id) @@ -219,7 +218,7 @@ class Chat(object): mime_type = mimetypes.guess_type(path)[0] msg = Message.new_empty(self.account, view_type="image") msg.set_file(path, mime_type) - sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) + sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg) if sent_id == 0: raise ValueError("message could not be sent") return Message.from_db(self.account, sent_id) @@ -230,7 +229,7 @@ class Chat(object): :param msg: the message to be prepared. :returns: :class:`deltachat.message.Message` instance. """ - msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg) + 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 @@ -266,7 +265,7 @@ class Chat(object): msg = Message.from_db(self.account, message.id) # pass 0 as chat-id because core-docs say it's ok when out-preparing - sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg) + sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg) if sent_id == 0: raise ValueError("message could not be sent") assert sent_id == msg.id @@ -280,9 +279,9 @@ class Chat(object): :returns: None """ if message is None: - lib.dc_set_draft(self._dc_context, self.id, ffi.NULL) + lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL) else: - lib.dc_set_draft(self._dc_context, self.id, message._dc_msg) + lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg) def get_draft(self): """ get draft message for this chat. @@ -290,7 +289,7 @@ class Chat(object): :param message: a :class:`Message` instance :returns: Message object or None (if no draft available) """ - x = lib.dc_get_draft(self._dc_context, self.id) + x = lib.dc_get_draft(self.account._dc_context, self.id) if x == ffi.NULL: return None dc_msg = ffi.gc(x, lib.dc_msg_unref) @@ -302,7 +301,7 @@ class Chat(object): :returns: list of :class:`deltachat.message.Message` objects for this chat. """ dc_array = ffi.gc( - lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0), + lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0), lib.dc_array_unref ) return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x))) @@ -312,18 +311,18 @@ class Chat(object): :returns: number of fresh messages """ - return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id) + return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id) def mark_noticed(self): """ mark all messages in this chat as noticed. Noticed messages are no longer fresh. """ - return lib.dc_marknoticed_chat(self._dc_context, self.id) + return lib.dc_marknoticed_chat(self.account._dc_context, self.id) def get_summary(self): """ return dictionary with summary information. """ - dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id) + dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id) s = from_dc_charpointer(dc_res) return json.loads(s) @@ -336,7 +335,7 @@ class Chat(object): :raises ValueError: if contact could not be added :returns: None """ - ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id) + ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id) if ret != 1: raise ValueError("could not add contact {!r} to chat".format(contact)) @@ -347,7 +346,7 @@ class Chat(object): :raises ValueError: if contact could not be removed :returns: None """ - ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id) + ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id) if ret != 1: raise ValueError("could not remove contact {!r} from chat".format(contact)) @@ -359,7 +358,7 @@ class Chat(object): """ from .contact import Contact dc_array = ffi.gc( - lib.dc_get_chat_contacts(self._dc_context, self.id), + lib.dc_get_chat_contacts(self.account._dc_context, self.id), lib.dc_array_unref ) return list(iter_array( @@ -378,7 +377,7 @@ class Chat(object): """ assert os.path.exists(img_path), img_path p = as_dc_charpointer(img_path) - res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p) + res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p) if res != 1: raise ValueError("Setting Profile Image {!r} failed".format(p)) @@ -391,7 +390,7 @@ class Chat(object): :raises ValueError: if profile image could not be reset :returns: None """ - res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL) + res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL) if res != 1: raise ValueError("Removing Profile Image failed") @@ -421,7 +420,7 @@ class Chat(object): """return True if this chat has location-sending enabled currently. :returns: True if location sending is enabled. """ - return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id) + return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id) def is_archived(self): """return True if this chat is archived. @@ -434,7 +433,7 @@ class Chat(object): all subsequent messages will carry a location with them. """ - lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds) + lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds) def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None): """return list of locations for the given contact in the given timespan. @@ -458,7 +457,7 @@ class Chat(object): else: contact_id = contact.id - dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to) + dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to) return [ Location( latitude=lib.dc_array_get_latitude(dc_array, i), diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 6e2cf1ae5..92e46f50c 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -12,22 +12,21 @@ class Contact(object): """ def __init__(self, account, id): self.account = account - self._dc_context = account._dc_context self.id = id def __eq__(self, other): - return self._dc_context == other._dc_context and self.id == other.id + return self.account._dc_context == other.account._dc_context and self.id == other.id def __ne__(self, other): return not (self == other) def __repr__(self): - return "".format(self.id, self.addr, self._dc_context) + return "".format(self.id, self.addr, self.account._dc_context) @property def _dc_contact(self): return ffi.gc( - lib.dc_get_contact(self._dc_context, self.id), + lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_contact_unref ) diff --git a/python/src/deltachat/eventlogger.py b/python/src/deltachat/eventlogger.py deleted file mode 100644 index 5490e7e72..000000000 --- a/python/src/deltachat/eventlogger.py +++ /dev/null @@ -1,137 +0,0 @@ -import deltachat -import threading -import time -import re -from queue import Queue, Empty -from .hookspec import account_hookimpl, global_hookimpl - - -@global_hookimpl -def dc_account_init(account): - # send all FFI events for this account to a plugin hook - def _ll_event(ctx, evt_name, data1, data2): - assert ctx == account._dc_context - ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2) - account._pm.hook.ac_process_ffi_event( - account=account, ffi_event=ffi_event - ) - deltachat.set_context_callback(account._dc_context, _ll_event) - - -@global_hookimpl -def dc_account_after_shutdown(dc_context): - deltachat.clear_context_callback(dc_context) - - -class FFIEvent: - def __init__(self, name, data1, data2): - self.name = name - self.data1 = data1 - self.data2 = data2 - - def __str__(self): - return "{name} data1={data1} data2={data2}".format(**self.__dict__) - - -class FFIEventLogger: - """ If you register an instance of this logger with an Account - you'll get all ffi-events printed. - """ - # to prevent garbled logging - _loglock = threading.RLock() - - def __init__(self, account, logid): - """ - :param logid: an optional logging prefix that should be used with - the default internal logging. - """ - self.account = account - self.logid = logid - self.init_time = time.time() - - @account_hookimpl - def ac_process_ffi_event(self, ffi_event): - self._log_event(ffi_event) - - def _log_event(self, ffi_event): - # don't show events that are anyway empty impls now - if ffi_event.name == "DC_EVENT_GET_STRING": - return - self.account.ac_log_line(str(ffi_event)) - - @account_hookimpl - def ac_log_line(self, message): - t = threading.currentThread() - tname = getattr(t, "name", t) - if tname == "MainThread": - tname = "MAIN" - elapsed = time.time() - self.init_time - locname = tname - if self.logid: - locname += "-" + self.logid - s = "{:2.2f} [{}] {}".format(elapsed, locname, message) - with self._loglock: - print(s, flush=True) - - -class FFIEventTracker: - def __init__(self, account, timeout=None): - self.account = account - self._timeout = timeout - self._event_queue = Queue() - - @account_hookimpl - def ac_process_ffi_event(self, ffi_event): - self._event_queue.put(ffi_event) - - def set_timeout(self, timeout): - self._timeout = timeout - - def consume_events(self, check_error=True): - while not self._event_queue.empty(): - self.get(check_error=check_error) - - def get(self, timeout=None, check_error=True): - timeout = timeout if timeout is not None else self._timeout - ev = self._event_queue.get(timeout=timeout) - if check_error and ev.name == "DC_EVENT_ERROR": - raise ValueError(str(ev)) - return ev - - def ensure_event_not_queued(self, event_name_regex): - __tracebackhide__ = True - rex = re.compile("(?:{}).*".format(event_name_regex)) - while 1: - try: - ev = self._event_queue.get(False) - except Empty: - break - else: - assert not rex.match(ev.name), "event found {}".format(ev) - - def get_matching(self, event_name_regex, check_error=True, timeout=None): - self.account.ac_log_line("-- waiting for event with regex: {} --".format(event_name_regex)) - rex = re.compile("(?:{}).*".format(event_name_regex)) - while 1: - ev = self.get(timeout=timeout, check_error=check_error) - if rex.match(ev.name): - return ev - - def get_info_matching(self, regex): - rex = re.compile("(?:{}).*".format(regex)) - while 1: - ev = self.get_matching("DC_EVENT_INFO") - if rex.match(ev.data2): - return ev - - def wait_next_incoming_message(self): - """ wait for and return next incoming message. """ - ev = self.get_matching("DC_EVENT_INCOMING_MSG") - return self.account.get_message_by_id(ev.data2) - - def wait_next_messages_changed(self): - """ wait for and return next message-changed message or None - if the event contains no msgid""" - ev = self.get_matching("DC_EVENT_MSGS_CHANGED") - if ev.data2 > 0: - return self.account.get_message_by_id(ev.data2) diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py new file mode 100644 index 000000000..967002f65 --- /dev/null +++ b/python/src/deltachat/events.py @@ -0,0 +1,217 @@ +import threading +import time +import re +from queue import Queue, Empty + +import deltachat +from .hookspec import account_hookimpl +from contextlib import contextmanager +from .capi import ffi, lib +from .message import map_system_message +from .cutil import from_dc_charpointer + + +class FFIEvent: + def __init__(self, name, data1, data2): + self.name = name + self.data1 = data1 + self.data2 = data2 + + def __str__(self): + return "{name} data1={data1} data2={data2}".format(**self.__dict__) + + +class FFIEventLogger: + """ If you register an instance of this logger with an Account + you'll get all ffi-events printed. + """ + # to prevent garbled logging + _loglock = threading.RLock() + + def __init__(self, account, logid): + """ + :param logid: an optional logging prefix that should be used with + the default internal logging. + """ + self.account = account + self.logid = logid + self.init_time = time.time() + + @account_hookimpl + def ac_process_ffi_event(self, ffi_event): + self.account.log(str(ffi_event)) + + @account_hookimpl + def ac_log_line(self, message): + t = threading.currentThread() + tname = getattr(t, "name", t) + if tname == "MainThread": + tname = "MAIN" + elapsed = time.time() - self.init_time + locname = tname + if self.logid: + locname += "-" + self.logid + s = "{:2.2f} [{}] {}".format(elapsed, locname, message) + with self._loglock: + print(s, flush=True) + + +class FFIEventTracker: + def __init__(self, account, timeout=None): + self.account = account + self._timeout = timeout + self._event_queue = Queue() + + @account_hookimpl + def ac_process_ffi_event(self, ffi_event): + self._event_queue.put(ffi_event) + + def set_timeout(self, timeout): + self._timeout = timeout + + def consume_events(self, check_error=True): + while not self._event_queue.empty(): + self.get(check_error=check_error) + + def get(self, timeout=None, check_error=True): + timeout = timeout if timeout is not None else self._timeout + ev = self._event_queue.get(timeout=timeout) + if check_error and ev.name == "DC_EVENT_ERROR": + raise ValueError("unexpected event: {}".format(ev)) + return ev + + def iter_events(self, timeout=None, check_error=True): + while 1: + yield self.get(timeout=timeout, check_error=check_error) + + def get_matching(self, event_name_regex, check_error=True, timeout=None): + rex = re.compile("(?:{}).*".format(event_name_regex)) + for ev in self.iter_events(timeout=timeout, check_error=check_error): + if rex.match(ev.name): + return ev + + def get_info_matching(self, regex): + rex = re.compile("(?:{}).*".format(regex)) + while 1: + ev = self.get_matching("DC_EVENT_INFO") + if rex.match(ev.data2): + return ev + + def ensure_event_not_queued(self, event_name_regex): + __tracebackhide__ = True + rex = re.compile("(?:{}).*".format(event_name_regex)) + while 1: + try: + ev = self._event_queue.get(False) + except Empty: + break + else: + assert not rex.match(ev.name), "event found {}".format(ev) + + def wait_securejoin_inviter_progress(self, target): + while 1: + event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS") + if event.data2 >= target: + print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account) + break + + def wait_next_incoming_message(self): + """ wait for and return next incoming message. """ + ev = self.get_matching("DC_EVENT_INCOMING_MSG") + return self.account.get_message_by_id(ev.data2) + + def wait_next_messages_changed(self): + """ wait for and return next message-changed message or None + if the event contains no msgid""" + ev = self.get_matching("DC_EVENT_MSGS_CHANGED") + if ev.data2 > 0: + return self.account.get_message_by_id(ev.data2) + + +class EventThread(threading.Thread): + """ Event Thread for an account. + + With each Account init this callback thread is started. + """ + def __init__(self, account): + self.account = account + super(EventThread, self).__init__(name="events") + self.setDaemon(True) + self.start() + + @contextmanager + def log_execution(self, message): + self.account.log(message + " START") + yield + self.account.log(message + " FINISHED") + + def wait(self): + if self == threading.current_thread(): + # we are in the callback thread and thus cannot + # wait for the thread-loop to finish. + return + self.join() + + def run(self): + """ get and run events until shutdown. """ + with self.log_execution("EVENT THREAD"): + self._inner_run() + + def _inner_run(self): + event_emitter = ffi.gc( + lib.dc_get_event_emitter(self.account._dc_context), + lib.dc_event_emitter_unref, + ) + while 1: + event = lib.dc_get_next_event(event_emitter) + if event == ffi.NULL: + break + evt = lib.dc_event_get_id(event) + data1 = lib.dc_event_get_data1_int(event) + # the following code relates to the deltachat/_build.py's helper + # function which provides us signature info of an event call + evt_name = deltachat.get_dc_event_name(evt) + if lib.dc_event_has_string_data(evt): + data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event)) + else: + data2 = lib.dc_event_get_data2_int(event) + + lib.dc_event_unref(event) + ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2) + try: + self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event) + for name, kwargs in self._map_ffi_event(ffi_event): + self.account.log("calling hook name={} kwargs={}".format(name, kwargs)) + hook = getattr(self.account._pm.hook, name) + hook(**kwargs) + except Exception: + if self.account._dc_context is not None: + raise + + def _map_ffi_event(self, ffi_event): + name = ffi_event.name + account = self.account + if name == "DC_EVENT_CONFIGURE_PROGRESS": + data1 = ffi_event.data1 + if data1 == 0 or data1 == 1000: + success = data1 == 1000 + yield "ac_configure_completed", dict(success=success) + elif name == "DC_EVENT_INCOMING_MSG": + msg = account.get_message_by_id(ffi_event.data2) + yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) + elif name == "DC_EVENT_MSGS_CHANGED": + if ffi_event.data2 != 0: + msg = account.get_message_by_id(ffi_event.data2) + if msg.is_outgoing(): + res = map_system_message(msg) + if res and res[0].startswith("ac_member"): + yield res + yield "ac_outgoing_message", dict(message=msg) + elif msg.is_in_fresh(): + yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) + elif name == "DC_EVENT_MSG_DELIVERED": + msg = account.get_message_by_id(ffi_event.data2) + yield "ac_message_delivered", dict(message=msg) + elif name == "DC_EVENT_CHAT_MODIFIED": + chat = account.get_chat_by_id(ffi_event.data1) + yield "ac_chat_modified", dict(chat=chat) diff --git a/python/src/deltachat/hookspec.py b/python/src/deltachat/hookspec.py index 00ec36d98..b20d56027 100644 --- a/python/src/deltachat/hookspec.py +++ b/python/src/deltachat/hookspec.py @@ -15,8 +15,9 @@ global_hookimpl = pluggy.HookimplMarker(global_spec_name) class PerAccount: """ per-Account-instance hook specifications. - Except for ac_process_ffi_event all hooks are executed - in the thread which calls Account.wait_shutdown(). + All hooks are executed in a dedicated Event thread. + Hooks are not allowed to block/last long as this + blocks overall event processing on the python side. """ @classmethod def _make_plugin_manager(cls): @@ -88,5 +89,5 @@ class Global: """ called when `Account::__init__()` function starts executing. """ @global_hookspec - def dc_account_after_shutdown(self, account, dc_context): + def dc_account_after_shutdown(self, account): """ Called after the account has been shutdown. """ diff --git a/python/src/deltachat/iothreads.py b/python/src/deltachat/iothreads.py deleted file mode 100644 index 1e9a864f1..000000000 --- a/python/src/deltachat/iothreads.py +++ /dev/null @@ -1,106 +0,0 @@ - -import threading -import time - -from contextlib import contextmanager - -from .capi import lib - - -class IOThreads: - def __init__(self, account): - self.account = account - self._dc_context = account._dc_context - self._thread_quitflag = False - self._name2thread = {} - - def is_started(self): - return len(self._name2thread) > 0 - - def start(self, callback_thread): - assert not self.is_started() - self._start_one_thread("inbox", self.imap_thread_run) - self._start_one_thread("smtp", self.smtp_thread_run) - - if callback_thread: - self._start_one_thread("cb", self.cb_thread_run) - - if int(self.account.get_config("mvbox_watch")): - self._start_one_thread("mvbox", self.mvbox_thread_run) - - if int(self.account.get_config("sentbox_watch")): - self._start_one_thread("sentbox", self.sentbox_thread_run) - - def _start_one_thread(self, name, func): - self._name2thread[name] = t = threading.Thread(target=func, name=name) - t.setDaemon(1) - t.start() - - @contextmanager - def log_execution(self, message): - self.account.ac_log_line(message + " START") - yield - self.account.ac_log_line(message + " FINISHED") - - def stop(self, wait=False): - self._thread_quitflag = True - - # Workaround for a race condition. Make sure that thread is - # not in between checking for quitflag and entering idle. - time.sleep(0.5) - - lib.dc_interrupt_imap_idle(self._dc_context) - lib.dc_interrupt_smtp_idle(self._dc_context) - if "mvbox" in self._name2thread: - lib.dc_interrupt_mvbox_idle(self._dc_context) - if "sentbox" in self._name2thread: - lib.dc_interrupt_sentbox_idle(self._dc_context) - if wait: - for name, thread in self._name2thread.items(): - if thread != threading.currentThread(): - thread.join() - - def cb_thread_run(self): - with self.log_execution("CALLBACK THREAD START"): - it = self.account.iter_events() - while not self._thread_quitflag: - try: - ev = next(it) - except StopIteration: - break - self.account.ac_log_line("calling hook name={} kwargs={}".format(ev.name, ev.kwargs)) - ev.call_hook() - - def imap_thread_run(self): - with self.log_execution("INBOX THREAD START"): - while not self._thread_quitflag: - lib.dc_perform_imap_jobs(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_imap_fetch(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_imap_idle(self._dc_context) - - def mvbox_thread_run(self): - with self.log_execution("MVBOX THREAD"): - while not self._thread_quitflag: - lib.dc_perform_mvbox_jobs(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_mvbox_fetch(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_mvbox_idle(self._dc_context) - - def sentbox_thread_run(self): - with self.log_execution("SENTBOX THREAD"): - while not self._thread_quitflag: - lib.dc_perform_sentbox_jobs(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_sentbox_fetch(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_sentbox_idle(self._dc_context) - - def smtp_thread_run(self): - with self.log_execution("SMTP THREAD"): - while not self._thread_quitflag: - lib.dc_perform_smtp_jobs(self._dc_context) - if not self._thread_quitflag: - lib.dc_perform_smtp_idle(self._dc_context) diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 91cc738c0..41bd51276 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -16,8 +16,7 @@ class Message(object): """ def __init__(self, account, dc_msg): self.account = account - self._dc_context = account._dc_context - assert isinstance(self._dc_context, ffi.CData) + assert isinstance(self.account._dc_context, ffi.CData) assert isinstance(dc_msg, ffi.CData) assert dc_msg != ffi.NULL self._dc_msg = dc_msg @@ -29,8 +28,10 @@ class Message(object): def __repr__(self): c = self.get_sender_contact() - return "".format( - self.id, c.id, c.addr, self.is_outgoing(), self.chat.id, self.chat.get_name()) + typ = "outgoing" if self.is_outgoing() else "incoming" + return "".format( + typ, self.is_system_message(), repr(self.text[:10]), + self.id, c.id, c.addr, self.chat.id, self.chat.get_name()) @classmethod def from_db(cls, account, id): @@ -58,7 +59,7 @@ class Message(object): """ self.account.create_chat_by_message(self) self._dc_msg = ffi.gc( - lib.dc_get_msg(self._dc_context, self.id), + lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref ) @@ -95,7 +96,7 @@ class Message(object): def is_system_message(self): """ return True if this message is a system/info message. """ - return lib.dc_msg_is_info(self._dc_msg) + return bool(lib.dc_msg_is_info(self._dc_msg)) def is_setup_message(self): """ return True if this message is a setup message. """ @@ -118,12 +119,12 @@ class Message(object): The text is multiline and may contain eg. the raw text of the message. """ - return from_dc_charpointer(lib.dc_get_msg_info(self._dc_context, self.id)) + return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id)) def continue_key_transfer(self, setup_code): """ extract key and use it as primary key for this account. """ res = lib.dc_continue_key_transfer( - self._dc_context, + self.account._dc_context, self.id, as_dc_charpointer(setup_code) ) @@ -158,7 +159,7 @@ class Message(object): :returns: email-mime message object (with headers only, no body). """ import email.parser - mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id) + mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id) if mime_headers: s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) if isinstance(s, bytes): @@ -201,7 +202,7 @@ class Message(object): else: # load message from db to get a fresh/current state dc_msg = ffi.gc( - lib.dc_get_msg(self._dc_context, self.id), + lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref ) return lib.dc_msg_get_state(dc_msg) diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 7208e8280..8a1939b08 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -13,10 +13,8 @@ import pytest import requests from . import Account, const -from .tracker import ConfigureTracker from .capi import lib -from .eventlogger import FFIEventLogger, FFIEventTracker -from _pytest.monkeypatch import MonkeyPatch +from .events import FFIEventLogger, FFIEventTracker from _pytest._code import Source import deltachat @@ -74,6 +72,9 @@ def pytest_configure(config): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): + if item.get_closest_marker("ignored"): + if not item.config.getvalue("ignored"): + pytest.skip("use --ignored to run this test") self.enable_logging(item) yield self.disable_logging(item) @@ -99,18 +100,16 @@ def pytest_report_header(config, startdir): summary = [] t = tempfile.mktemp() - m = MonkeyPatch() try: - m.setattr(sys.stdout, "write", lambda x: len(x)) ac = Account(t) info = ac.get_info() ac.shutdown() finally: - m.undo() os.remove(t) - summary.extend(['Deltachat core={} sqlite={}'.format( + summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format( info['deltachat_core_version'], info['sqlite_version'], + info['journal_mode'], )]) cfg = config.option.liveconfig @@ -231,7 +230,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): def make_account(self, path, logid, quiet=False): ac = Account(path, logging=self._logging) ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac)) - ac._configtracker = ac.add_account_plugin(ConfigureTracker()) + ac.addr = ac.get_self_contact().addr if not quiet: ac.add_account_plugin(FFIEventLogger(ac, logid=logid)) self._accounts.append(ac) @@ -302,24 +301,33 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): configdict["mvbox_move"] = str(int(move)) configdict["sentbox_watch"] = str(int(sentbox)) ac.update_config(configdict) - ac.start() + ac.configure() return ac def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False): ac1 = self.get_online_configuring_account( pre_generated_key=pre_generated_key, mvbox=mvbox, move=move) - ac1._configtracker.wait_imap_connected() - ac1._configtracker.wait_smtp_connected() - ac1._configtracker.wait_finish() + ac1.wait_configure_finish() + ac1.start_io() return ac1 def get_two_online_accounts(self, move=False, quiet=False): ac1 = self.get_online_configuring_account(move=True, quiet=quiet) ac2 = self.get_online_configuring_account(quiet=quiet) - ac1._configtracker.wait_finish() - ac2._configtracker.wait_finish() + ac1.wait_configure_finish() + ac1.start_io() + ac2.wait_configure_finish() + ac2.start_io() return ac1, ac2 + def get_many_online_accounts(self, num, move=True, quiet=True): + accounts = [self.get_online_configuring_account(move=move, quiet=quiet) + for i in range(num)] + for acc in accounts: + acc._configtracker.wait_finish() + acc.start_io() + return accounts + def clone_online_account(self, account, pre_generated_key=True): self.live_count += 1 tmpdb = tmpdir.join("livedb%d" % self.live_count) @@ -335,7 +343,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): mvbox_move=account.get_config("mvbox_move"), sentbox_watch=account.get_config("sentbox_watch"), )) - ac.start() + ac.configure() return ac def run_bot_process(self, module, ffi=True): @@ -392,6 +400,7 @@ class BotProcess: break line = line.strip() self.stdout_queue.put(line) + print("bot-stdout: ", line) finally: self.stdout_queue.put(None) diff --git a/python/src/deltachat/tracker.py b/python/src/deltachat/tracker.py index 4570f3d4c..bc8122b25 100644 --- a/python/src/deltachat/tracker.py +++ b/python/src/deltachat/tracker.py @@ -18,7 +18,7 @@ class ImexTracker: if ffi_event.name == "DC_EVENT_IMEX_PROGRESS": self._imex_events.put(ffi_event.data1) elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN": - self._imex_events.put(ffi_event.data1) + self._imex_events.put(ffi_event.data2) def wait_finish(self, progress_timeout=60): """ Return list of written files, raise ValueError if ExportFailed. """ @@ -45,6 +45,7 @@ class ConfigureTracker: self._smtp_finished = Event() self._imap_finished = Event() self._ffi_events = [] + self._progress = Queue() @account_hookimpl def ac_process_ffi_event(self, ffi_event): @@ -53,6 +54,8 @@ class ConfigureTracker: self._smtp_finished.set() elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED": self._imap_finished.set() + elif ffi_event.name == "DC_EVENT_CONFIGURE_PROGRESS": + self._progress.put(ffi_event.data1) @account_hookimpl def ac_configure_completed(self, success): @@ -66,6 +69,12 @@ class ConfigureTracker: """ wait until smtp is configured. """ self._imap_finished.wait() + def wait_progress(self, data1=None): + while 1: + evdata = self._progress.get() + if data1 is None or evdata == data1: + break + def wait_finish(self): """ wait until configure is completed. diff --git a/python/tests/auditwheels.py b/python/tests/auditwheels.py index eb4f5055a..bb8549b1c 100644 --- a/python/tests/auditwheels.py +++ b/python/tests/auditwheels.py @@ -10,4 +10,6 @@ if __name__ == "__main__": for relpath in os.listdir(workspacedir): if relpath.startswith("deltachat"): p = os.path.join(workspacedir, relpath) - subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir]) + subprocess.check_call( + ["auditwheel", "repair", p, "-w", workspacedir, + "--plat", "manylinux2014_x86_64"]) diff --git a/python/tests/conftest.py b/python/tests/conftest.py deleted file mode 100644 index b32e2fd0d..000000000 --- a/python/tests/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import print_function - - -def wait_configuration_progress(account, min_target, max_target=1001): - min_target = min(min_target, max_target) - while 1: - event = account._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS") - if event.data1 >= min_target and event.data1 <= max_target: - print("** CONFIG PROGRESS {}".format(min_target), account) - break - - -def wait_securejoin_inviter_progress(account, target): - while 1: - event = account._evtracker.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS") - if event.data2 >= target: - print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account) - break diff --git a/python/tests/stress_test_db.py b/python/tests/stress_test_db.py new file mode 100644 index 000000000..178ddcba6 --- /dev/null +++ b/python/tests/stress_test_db.py @@ -0,0 +1,135 @@ + +import time +import threading +import pytest +import os +from queue import Queue, Empty + +import deltachat + + +def test_db_busy_error(acfactory, tmpdir): + starttime = time.time() + log_lock = threading.RLock() + + def log(string): + with log_lock: + print("%3.2f %s" % (time.time() - starttime, string)) + + # make a number of accounts + accounts = acfactory.get_many_online_accounts(3, quiet=True) + log("created %s accounts" % len(accounts)) + + # put a bigfile into each account + for acc in accounts: + acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile") + with open(acc.bigfile, "wb") as f: + f.write(b"01234567890"*1000_000) + log("created %s bigfiles" % len(accounts)) + + contact_addrs = [acc.get_self_contact().addr for acc in accounts] + chat = accounts[0].create_group_chat("stress-group") + for addr in contact_addrs[1:]: + chat.add_contact(chat.account.create_contact(addr)) + + # setup auto-responder bots which report back failures/actions + report_queue = Queue() + + def report_func(replier, report_type, *report_args): + report_queue.put((replier, report_type, report_args)) + + # each replier receives all events and sends report events to receive_queue + repliers = [] + for acc in accounts: + replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func) + acc.add_account_plugin(replier) + repliers.append(replier) + + # kick off message sending + # after which repliers will reply to each other + chat.send_text("hello") + + alive_count = len(accounts) + while alive_count > 0: + try: + replier, report_type, report_args = report_queue.get(timeout=10) + except Empty: + log("timeout waiting for next event") + pytest.fail("timeout exceeded") + if report_type == ReportType.exit: + replier.log("EXIT") + elif report_type == ReportType.ffi_error: + replier.log("ERROR: {}".format(report_args[0])) + elif report_type == ReportType.message_echo: + continue + else: + raise ValueError("{} unknown report type {}, args={}".format( + addr, report_type, report_args + )) + alive_count -= 1 + replier.log("shutting down") + replier.account.shutdown() + replier.log("shut down complete, remaining={}".format(alive_count)) + + +class ReportType: + exit = "exit" + ffi_error = "ffi-error" + message_echo = "message-echo" + + +class AutoReplier: + def __init__(self, account, log, num_send, num_bigfiles, report_func): + self.account = account + self._log = log + self.report_func = report_func + self.num_send = num_send + self.num_bigfiles = num_bigfiles + self.current_sent = 0 + self.addr = self.account.get_self_contact().addr + + self._thread = threading.Thread( + name="Stats{}".format(self.account), + target=self.thread_stats + ) + self._thread.setDaemon(True) + self._thread.start() + + def log(self, message): + self._log("{} {}".format(self.addr, message)) + + def thread_stats(self): + # XXX later use, for now we just quit + return + while 1: + time.sleep(1.0) + break + + @deltachat.account_hookimpl + def ac_incoming_message(self, message): + if self.current_sent >= self.num_send: + self.report_func(self, ReportType.exit) + return + message.accept_sender_contact() + message.mark_seen() + self.log("incoming message: {}".format(message)) + + self.current_sent += 1 + # we are still alive, let's send a reply + if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0: + message.chat.send_text("send big file as reply to: {}".format(message.text)) + msg = message.chat.send_file(self.account.bigfile) + else: + msg = message.chat.send_text("got message id {}, small text reply".format(message.id)) + assert msg.text + self.log("message-sent: {}".format(msg)) + self.report_func(self, ReportType.message_echo) + if self.current_sent >= self.num_send: + self.report_func(self, ReportType.exit) + return + + @deltachat.account_hookimpl + def ac_process_ffi_event(self, ffi_event): + self.log(ffi_event) + if ffi_event.name == "DC_EVENT_ERROR": + self.report_func(self, ReportType.ffi_error, ffi_event) diff --git a/python/tests/test_account.py b/python/tests/test_account.py index a3378f2f5..aa124ab74 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -7,8 +7,6 @@ from deltachat import const, Account from deltachat.message import Message from deltachat.hookspec import account_hookimpl from datetime import datetime, timedelta -from conftest import (wait_configuration_progress, - wait_securejoin_inviter_progress) @pytest.mark.parametrize("msgtext,res", [ @@ -398,11 +396,9 @@ class TestOfflineChat: with bin.open("w") as f: f.write("\00123" * 10000) msg = chat.send_file(bin.strpath) - contact = msg.get_sender_contact() assert contact == ac1.get_self_contact() assert not backupdir.listdir() - path = ac1.export_all(backupdir.strpath) assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account() @@ -475,8 +471,17 @@ class TestOfflineChat: num_contacts = len(chat.get_contacts()) assert num_contacts == 11 - # perform plugin hooks - ac1._handle_current_events() + # let's make sure the events perform plugin hooks + def wait_events(cond): + now = time.time() + while time.time() < now + 5: + if cond(): + break + time.sleep(0.1) + else: + pytest.fail("failed to get events") + + wait_events(lambda: len(in_list) == 10) assert len(in_list) == 10 chat_contacts = chat.get_contacts() @@ -495,7 +500,7 @@ class TestOfflineChat: chat.remove_contact(contacts[3]) assert len(chat.get_contacts()) == 9 - ac1._handle_current_events() + wait_events(lambda: len(in_list) == 2) assert len(in_list) == 2 assert in_list[0][0] == "removed" assert in_list[0][1] == chat @@ -514,11 +519,6 @@ class TestOnlineAccount: ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr"))) return chat - def test_double_iter_events(self, acfactory): - ac1 = acfactory.get_one_online_account() - with pytest.raises(RuntimeError): - next(ac1.iter_events()) - @pytest.mark.ignored def test_configure_generate_key(self, acfactory, lp): # A slow test which will generate new keys. @@ -532,8 +532,10 @@ class TestOnlineAccount: ) # rsa key gen can be slow especially on CI, adjust timeout ac1._evtracker.set_timeout(120) - wait_configuration_progress(ac1, 1000) - wait_configuration_progress(ac2, 1000) + ac1.wait_configure_finish() + ac1.start_io() + ac2.wait_configure_finish() + ac2.start_io() chat = self.get_chat(ac1, ac2, both_created=True) lp.sec("ac1: send unencrypted message to ac2") @@ -562,12 +564,16 @@ class TestOnlineAccount: def test_configure_canceled(self, acfactory): ac1 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac1, 200) + ac1._configtracker.wait_progress() ac1.stop_ongoing() - wait_configuration_progress(ac1, 0, 0) + try: + ac1.wait_configure_finish() + except Exception: + pass def test_export_import_self_keys(self, acfactory, tmpdir): ac1, ac2 = acfactory.get_two_online_accounts() + dir = tmpdir.mkdir("exportdir") export_files = ac1.export_self_keys(dir.strpath) assert len(export_files) == 2 @@ -584,9 +590,12 @@ class TestOnlineAccount: # are copied to it via BCC. ac1_clone = acfactory.clone_online_account(ac1) - wait_configuration_progress(ac1, 1000) - wait_configuration_progress(ac2, 1000) - wait_configuration_progress(ac1_clone, 1000) + ac1.wait_configure_finish() + ac1.start_io() + ac2.wait_configure_finish() + ac2.start_io() + ac1_clone.wait_configure_finish() + ac1_clone.start_io() chat = self.get_chat(ac1, ac2) @@ -690,10 +699,12 @@ class TestOnlineAccount: ac2 = acfactory.get_online_configuring_account() lp.sec("ac2: waiting for configuration") - wait_configuration_progress(ac2, 1000) + ac2.wait_configure_finish() + ac2.start_io() lp.sec("ac1: waiting for configuration") - wait_configuration_progress(ac1, 1000) + ac1.wait_configure_finish() + ac1.start_io() lp.sec("ac1: send message and wait for ac2 to receive it") chat = self.get_chat(ac1, ac2) @@ -705,8 +716,10 @@ class TestOnlineAccount: def test_move_works(self, acfactory): ac1 = acfactory.get_online_configuring_account() ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True) - wait_configuration_progress(ac2, 1000) - wait_configuration_progress(ac1, 1000) + ac2.wait_configure_finish() + ac2.start_io() + ac1.wait_configure_finish() + ac1.start_io() chat = self.get_chat(ac1, ac2) chat.send_text("message1") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") @@ -717,8 +730,11 @@ class TestOnlineAccount: ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True) ac1.set_config("bcc_self", "1") ac2 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac2, 1000) - wait_configuration_progress(ac1, 1000) + ac2.wait_configure_finish() + ac2.start_io() + ac1.wait_configure_finish() + ac1.start_io() + chat = self.get_chat(ac1, ac2) chat.send_text("message1") chat.send_text("message2") @@ -793,8 +809,9 @@ class TestOnlineAccount: ac1.empty_server_folders(inbox=True, mvbox=True) ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED") ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED") - boxes = sorted([ev1.data2, ev2.data2]) - assert boxes == ["DeltaChat", "INBOX"] + boxes = [ev1.data2, ev2.data2] + boxes.remove("INBOX") + assert len(boxes) == 1 and boxes[0].endswith("DeltaChat") def test_send_and_receive_message_markseen(self, acfactory, lp): ac1, ac2 = acfactory.get_two_online_accounts() @@ -1047,6 +1064,35 @@ class TestOnlineAccount: assert mime.get_all("From") assert mime.get_all("Received") + def test_send_mark_seen_clean_incoming_events(self, acfactory, lp, data): + ac1, ac2 = acfactory.get_two_online_accounts() + chat = self.get_chat(ac1, ac2, both_created=True) + + message_queue = queue.Queue() + + class InPlugin: + @account_hookimpl + def ac_incoming_message(self, message): + message_queue.put(message) + + ac1.add_account_plugin(InPlugin()) + + lp.sec("sending one message from ac1 to ac2") + chat.send_text("hello") + + lp.sec("ac2: waiting to receive") + msg = ac2._evtracker.wait_next_incoming_message() + assert msg.text == "hello" + + lp.sec("ac2: mark seen {}".format(msg)) + msg.mark_seen() + + for ev in ac1._evtracker.iter_events(): + if ev.name == "DC_EVENT_INCOMING_MSG": + pytest.fail("MDN arrived as regular incoming message") + elif ev.name == "DC_EVENT_MSG_READ": + break + def test_send_and_receive_image(self, acfactory, lp, data): ac1, ac2 = acfactory.get_two_online_accounts() chat = self.get_chat(ac1, ac2) @@ -1096,8 +1142,7 @@ class TestOnlineAccount: assert m == msg_in def test_import_export_online_all(self, acfactory, tmpdir, lp): - ac1 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac1, 1000) + ac1 = acfactory.get_one_online_account() lp.sec("create some chat content") contact1 = ac1.create_contact("some1@hello.com", name="some1") @@ -1145,8 +1190,11 @@ class TestOnlineAccount: # as of Jul2019 ac1 = acfactory.get_online_configuring_account() ac2 = acfactory.clone_online_account(ac1) - wait_configuration_progress(ac2, 1000) - wait_configuration_progress(ac1, 1000) + ac2.wait_configure_finish() + ac2.start_io() + ac1.wait_configure_finish() + ac1.start_io() + lp.sec("trigger ac setup message and return setupcode") assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] setup_code = ac1.initiate_key_transfer() @@ -1168,8 +1216,10 @@ class TestOnlineAccount: ac1 = acfactory.get_online_configuring_account() ac2 = acfactory.clone_online_account(ac1) ac2._evtracker.set_timeout(30) - wait_configuration_progress(ac2, 1000) - wait_configuration_progress(ac1, 1000) + ac2.wait_configure_finish() + ac2.start_io() + ac1.wait_configure_finish() + ac1.start_io() lp.sec("trigger ac setup message but ignore") assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] @@ -1195,7 +1245,7 @@ class TestOnlineAccount: lp.sec("ac2: start QR-code based setup contact protocol") ch = ac2.qr_setup_contact(qr) assert ch.id >= 10 - wait_securejoin_inviter_progress(ac1, 1000) + ac1._evtracker.wait_securejoin_inviter_progress(1000) def test_qr_join_chat(self, acfactory, lp): ac1, ac2 = acfactory.get_two_online_accounts() @@ -1204,11 +1254,12 @@ class TestOnlineAccount: qr = chat.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") ch = ac2.qr_join_chat(qr) + lp.sec("ac2: qr_join_chat() returned") assert ch.id >= 10 # check that at least some of the handshake messages are deleted ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") - wait_securejoin_inviter_progress(ac1, 1000) + ac1._evtracker.wait_securejoin_inviter_progress(1000) def test_qr_verified_group_and_chatting(self, acfactory, lp): ac1, ac2 = acfactory.get_two_online_accounts() @@ -1219,7 +1270,7 @@ class TestOnlineAccount: lp.sec("ac2: start QR-code based join-group protocol") chat2 = ac2.qr_join_chat(qr) assert chat2.id >= 10 - wait_securejoin_inviter_progress(ac1, 1000) + ac1._evtracker.wait_securejoin_inviter_progress(1000) lp.sec("ac2: read member added message") msg = ac2._evtracker.wait_next_incoming_message() @@ -1479,7 +1530,8 @@ class TestGroupStressTests: lp.sec("creating and configuring five accounts") accounts = [acfactory.get_online_configuring_account() for i in range(5)] for acc in accounts: - wait_configuration_progress(acc, 1000) + acc.wait_configure_finish() + acc.start_io() ac1 = accounts.pop() lp.sec("ac1: setting up contacts with 4 other members") @@ -1572,6 +1624,9 @@ class TestGroupStressTests: # Message should be encrypted because keys of other members are gossiped assert msg.is_encrypted() + for account in accounts: + account.shutdown() + def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp): """ Test that user recreates group member list when it joins the group again. @@ -1583,7 +1638,8 @@ class TestGroupStressTests: lp.sec("creating and configuring five accounts") accounts = [acfactory.get_online_configuring_account() for i in range(3)] for acc in accounts: - wait_configuration_progress(acc, 1000) + acc.wait_configure_finish() + acc.start_io() ac1 = accounts.pop() lp.sec("ac1: setting up contacts with 2 other members") @@ -1646,31 +1702,35 @@ class TestGroupStressTests: assert len(msg.chat.get_contacts()) == len(chat.get_contacts()) + ac1.shutdown() + ac2.shutdown() + ac3.shutdown() + class TestOnlineConfigureFails: def test_invalid_password(self, acfactory): ac1, configdict = acfactory.get_online_config() ac1.update_config(dict(addr=configdict["addr"], mail_pw="123")) - ac1.start() - wait_configuration_progress(ac1, 500) + ac1.configure() + ac1._configtracker.wait_progress(500) + ac1._configtracker.wait_progress(0) ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") assert "cannot login" in ev.data2.lower() - wait_configuration_progress(ac1, 0, 0) def test_invalid_user(self, acfactory): ac1, configdict = acfactory.get_online_config() ac1.update_config(dict(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"])) - ac1.start() - wait_configuration_progress(ac1, 500) + ac1.configure() + ac1._configtracker.wait_progress(500) + ac1._configtracker.wait_progress(0) ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") assert "cannot login" in ev.data2.lower() - wait_configuration_progress(ac1, 0, 0) def test_invalid_domain(self, acfactory): ac1, configdict = acfactory.get_online_config() ac1.update_config((dict(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"]))) - ac1.start() - wait_configuration_progress(ac1, 500) + ac1.configure() + ac1._configtracker.wait_progress(500) + ac1._configtracker.wait_progress(0) ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") assert "could not connect" in ev.data2.lower() - wait_configuration_progress(ac1, 0, 0) diff --git a/python/tests/test_increation.py b/python/tests/test_increation.py index 6fb96e1fe..99d49771d 100644 --- a/python/tests/test_increation.py +++ b/python/tests/test_increation.py @@ -6,24 +6,36 @@ import shutil import pytest from filecmp import cmp -from conftest import wait_configuration_progress from deltachat import const -def wait_msgs_changed(account, chat_id, msg_id=None): - ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") - assert ev.data1 == chat_id - if msg_id is not None: - assert ev.data2 == msg_id - return ev.data2 +def wait_msg_delivered(account, msg_list): + """ wait for one or more MSG_DELIVERED events to match msg_list contents. """ + msg_list = list(msg_list) + while msg_list: + ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") + msg_list.remove((ev.data1, ev.data2)) + + +def wait_msgs_changed(account, msgs_list): + """ wait for one or more MSGS_CHANGED events to match msgs_list contents. """ + account.log("waiting for msgs_list={}".format(msgs_list)) + msgs_list = list(msgs_list) + while msgs_list: + ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") + for i, (data1, data2) in enumerate(msgs_list): + if ev.data1 == data1: + if data2 is None or ev.data2 == data2: + del msgs_list[i] + break + else: + account.log("waiting mismatch data1={} data2={}".format(data1, data2)) + return ev.data1, ev.data2 class TestOnlineInCreation: def test_increation_not_blobdir(self, tmpdir, acfactory, lp): - ac1 = acfactory.get_online_configuring_account() - ac2 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac1, 1000) - wait_configuration_progress(ac2, 1000) + ac1, ac2 = acfactory.get_two_online_accounts() c2 = ac1.create_contact(email=ac2.get_config("addr")) chat = ac1.create_chat_by_contact(c2) @@ -35,10 +47,7 @@ class TestOnlineInCreation: chat.prepare_message_file(src.strpath) def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp): - ac1 = acfactory.get_online_configuring_account() - ac2 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac1, 1000) - wait_configuration_progress(ac2, 1000) + ac1, ac2 = acfactory.get_two_online_accounts() c2 = ac1.create_contact(email=ac2.get_config("addr")) chat = ac1.create_chat_by_contact(c2) @@ -53,15 +62,12 @@ class TestOnlineInCreation: assert os.path.exists(blob_src), "file.txt not copied to blobdir" def test_forward_increation(self, acfactory, data, lp): - ac1 = acfactory.get_online_configuring_account() - ac2 = acfactory.get_online_configuring_account() - wait_configuration_progress(ac1, 1000) - wait_configuration_progress(ac2, 1000) + ac1, ac2 = acfactory.get_two_online_accounts() c2 = ac1.create_contact(email=ac2.get_config("addr")) chat = ac1.create_chat_by_contact(c2) assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL - wait_msgs_changed(ac1, 0, 0) # why no chat id? + wait_msgs_changed(ac1, [(0, 0)]) # why no chat id? lp.sec("create a message with a file in creation") orig = data.get_path("d.png") @@ -70,19 +76,16 @@ class TestOnlineInCreation: fp.write("preparing") prepared_original = chat.prepare_message_file(path) assert prepared_original.is_out_preparing() - wait_msgs_changed(ac1, chat.id, prepared_original.id) + wait_msgs_changed(ac1, [(chat.id, prepared_original.id)]) lp.sec("forward the message while still in creation") chat2 = ac1.create_group_chat("newgroup") chat2.add_contact(c2) - wait_msgs_changed(ac1, 0, 0) # why not chat id? + wait_msgs_changed(ac1, [(0, 0)]) # why not chat id? ac1.forward_messages([prepared_original], chat2) # XXX there might be two EVENT_MSGS_CHANGED and only one of them # is the one caused by forwarding - forwarded_id = wait_msgs_changed(ac1, chat2.id) - if forwarded_id == 0: - forwarded_id = wait_msgs_changed(ac1, chat2.id) - assert forwarded_id + _, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)]) forwarded_msg = ac1.get_message_by_id(forwarded_id) assert forwarded_msg.is_out_preparing() @@ -91,20 +94,18 @@ class TestOnlineInCreation: shutil.copyfile(orig, path) chat.send_prepared(prepared_original) assert prepared_original.is_out_pending() or prepared_original.is_out_delivered() - wait_msgs_changed(ac1, chat.id, prepared_original.id) - lp.sec("expect the forwarded message to be sent now too") - wait_msgs_changed(ac1, chat2.id, forwarded_id) + lp.sec("check that both forwarded and original message are proper.") + wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)]) + fwd_msg = ac1.get_message_by_id(forwarded_id) assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered() - lp.sec("wait for the messages to be delivered to SMTP") - ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") - assert ev.data1 == chat.id - assert ev.data2 == prepared_original.id - ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") - assert ev.data1 == chat2.id - assert ev.data2 == forwarded_id + lp.sec("wait for both messages to be delivered to SMTP") + wait_msg_delivered(ac1, [ + (chat2.id, forwarded_id), + (chat.id, prepared_original.id) + ]) lp.sec("wait1 for original or forwarded messages to arrive") ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py index 48865ed44..2e7640e36 100644 --- a/python/tests/test_lowlevel.py +++ b/python/tests/test_lowlevel.py @@ -1,75 +1,52 @@ from __future__ import print_function -from deltachat import capi, cutil, const, set_context_callback, clear_context_callback + +from queue import Queue +from deltachat import capi, cutil, const from deltachat import register_global_plugin from deltachat.hookspec import global_hookimpl from deltachat.capi import ffi from deltachat.capi import lib +# from deltachat.account import EventLogger def test_empty_context(): ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL) - capi.lib.dc_close(ctx) - - -def test_callback_None2int(): - ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL) - set_context_callback(ctx, lambda *args: None) - capi.lib.dc_close(ctx) - clear_context_callback(ctx) + capi.lib.dc_context_unref(ctx) def test_dc_close_events(tmpdir, acfactory): ac1 = acfactory.get_unconfigured_account() # register after_shutdown function - shutdowns = [] + shutdowns = Queue() class ShutdownPlugin: @global_hookimpl def dc_account_after_shutdown(self, account): assert account._dc_context is None - shutdowns.append(account) + shutdowns.put(account) register_global_plugin(ShutdownPlugin()) assert hasattr(ac1, "_dc_context") ac1.shutdown() - assert shutdowns == [ac1] - - def find(info_string): - evlog = ac1._evtracker - while 1: - ev = evlog.get_matching("DC_EVENT_INFO", check_error=False) - data2 = ev.data2 - if info_string in data2: - return - else: - print("skipping event", ev) - - find("disconnecting inbox-thread") - find("disconnecting sentbox-thread") - find("disconnecting mvbox-thread") - find("disconnecting SMTP") - find("Database closed") + shutdowns.get(timeout=2) def test_wrong_db(tmpdir): - dc_context = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), - lib.dc_context_unref, - ) p = tmpdir.join("hello.db") # write an invalid database file p.write("x123" * 10) - assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL) + + assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL) def test_empty_blobdir(tmpdir): + db_fname = tmpdir.join("hello.db") # Apparently some client code expects this to be the same as passing NULL. ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), + lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""), lib.dc_context_unref, ) - db_fname = tmpdir.join("hello.db") - assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"") + assert ctx != ffi.NULL def test_event_defines(): @@ -78,17 +55,20 @@ def test_event_defines(): def test_sig(): - sig = capi.lib.dc_get_event_signature_types - assert sig(const.DC_EVENT_INFO) == 2 - assert sig(const.DC_EVENT_WARNING) == 2 - assert sig(const.DC_EVENT_ERROR) == 2 - assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2 - assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2 - assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2 + sig = capi.lib.dc_event_has_string_data + assert not sig(const.DC_EVENT_MSGS_CHANGED) + assert sig(const.DC_EVENT_INFO) + assert sig(const.DC_EVENT_WARNING) + assert sig(const.DC_EVENT_ERROR) + assert sig(const.DC_EVENT_SMTP_CONNECTED) + assert sig(const.DC_EVENT_IMAP_CONNECTED) + assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) + assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN) def test_markseen_invalid_message_ids(acfactory): ac1 = acfactory.get_configured_offline_account() + contact1 = ac1.create_contact(email="some1@example.com", name="some1") chat = ac1.create_chat_by_contact(contact1) chat.send_text("one messae") @@ -107,47 +87,18 @@ def test_get_special_message_id_returns_empty_message(acfactory): def test_provider_info_none(): ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), + lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL), lib.dc_context_unref, ) assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL -def test_get_info_closed(): - ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), - lib.dc_context_unref, - ) - info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) - assert 'deltachat_core_version' in info - assert 'database_dir' not in info - - def test_get_info_open(tmpdir): + db_fname = tmpdir.join("test.db") ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), + lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL), lib.dc_context_unref, ) - db_fname = tmpdir.join("test.db") - lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) assert 'deltachat_core_version' in info assert 'database_dir' in info - - -def test_is_open_closed(): - ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), - lib.dc_context_unref, - ) - assert lib.dc_is_open(ctx) == 0 - - -def test_is_open_actually_open(tmpdir): - ctx = ffi.gc( - lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), - lib.dc_context_unref, - ) - db_fname = tmpdir.join("test.db") - lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL) - assert lib.dc_is_open(ctx) == 1 diff --git a/rust-toolchain b/rust-toolchain index 7b70b3322..3987c4729 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2020-03-12 +1.43.1 diff --git a/src/blob.rs b/src/blob.rs index f5bd2f47f..bbb5635c9 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -2,9 +2,10 @@ use std::ffi::OsStr; use std::fmt; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; + +use async_std::path::{Path, PathBuf}; +use async_std::prelude::*; +use async_std::{fs, io}; use image::GenericImageView; use thiserror::Error; @@ -43,15 +44,16 @@ impl<'a> BlobObject<'a> { /// [BlobError::WriteFailure] is used when the file could not /// be written to. You can expect [BlobError.cause] to contain an /// underlying error. - pub fn create( + pub async fn create( context: &'a Context, suggested_name: impl AsRef, data: &[u8], ) -> std::result::Result, BlobError> { let blobdir = context.get_blobdir(); let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref()); - let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?; + let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?; file.write_all(data) + .await .map_err(|err| BlobError::WriteFailure { blobdir: blobdir.to_path_buf(), blobname: name.clone(), @@ -61,12 +63,16 @@ impl<'a> BlobObject<'a> { blobdir, name: format!("$BLOBDIR/{}", name), }; - context.call_cb(Event::NewBlobFile(blob.as_name().to_string())); + context.emit_event(Event::NewBlobFile(blob.as_name().to_string())); Ok(blob) } // Creates a new file, returning a tuple of the name and the handle. - fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> { + async fn create_new_file( + dir: &Path, + stem: &str, + ext: &str, + ) -> Result<(String, fs::File), BlobError> { let max_attempt = 15; let mut name = format!("{}{}", stem, ext); for attempt in 0..max_attempt { @@ -75,6 +81,7 @@ impl<'a> BlobObject<'a> { .create_new(true) .write(true) .open(&path) + .await { Ok(file) => return Ok((name, file)), Err(err) => { @@ -110,37 +117,41 @@ impl<'a> BlobObject<'a> { /// In addition to the errors in [BlobObject::create] the /// [BlobError::CopyFailure] is used when the data can not be /// copied. - pub fn create_and_copy( + pub async fn create_and_copy( context: &'a Context, src: impl AsRef, ) -> std::result::Result, BlobError> { - let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure { - blobdir: context.get_blobdir().to_path_buf(), - blobname: String::from(""), - src: src.as_ref().to_path_buf(), - cause: err, - })?; + let mut src_file = + fs::File::open(src.as_ref()) + .await + .map_err(|err| BlobError::CopyFailure { + blobdir: context.get_blobdir().to_path_buf(), + blobname: String::from(""), + src: src.as_ref().to_path_buf(), + cause: err, + })?; let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy()); - let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?; + let (name, mut dst_file) = + BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?; let name_for_err = name.clone(); - std::io::copy(&mut src_file, &mut dst_file).map_err(|err| { + if let Err(err) = io::copy(&mut src_file, &mut dst_file).await { { // Attempt to remove the failed file, swallow errors resulting from that. let path = context.get_blobdir().join(&name_for_err); - fs::remove_file(path).ok(); + fs::remove_file(path).await.ok(); } - BlobError::CopyFailure { + return Err(BlobError::CopyFailure { blobdir: context.get_blobdir().to_path_buf(), blobname: name_for_err, src: src.as_ref().to_path_buf(), cause: err, - } - })?; + }); + } let blob = BlobObject { blobdir: context.get_blobdir(), name: format!("$BLOBDIR/{}", name), }; - context.call_cb(Event::NewBlobFile(blob.as_name().to_string())); + context.emit_event(Event::NewBlobFile(blob.as_name().to_string())); Ok(blob) } @@ -158,14 +169,14 @@ impl<'a> BlobObject<'a> { /// This merely delegates to the [BlobObject::create_and_copy] and /// the [BlobObject::from_path] methods. See those for possible /// errors. - pub fn new_from_path( + pub async fn new_from_path( context: &Context, src: impl AsRef, - ) -> std::result::Result { + ) -> std::result::Result, BlobError> { if src.as_ref().starts_with(context.get_blobdir()) { BlobObject::from_path(context, src) } else { - BlobObject::create_and_copy(context, src) + BlobObject::create_and_copy(context, src).await } } @@ -418,58 +429,71 @@ mod tests { use crate::test_utils::*; - #[test] - fn test_create() { - let t = dummy_context(); - let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap(); + #[async_std::test] + async fn test_create() { + let t = dummy_context().await; + let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap(); let fname = t.ctx.get_blobdir().join("foo"); - let data = fs::read(fname).unwrap(); + let data = fs::read(fname).await.unwrap(); assert_eq!(data, b"hello"); assert_eq!(blob.as_name(), "$BLOBDIR/foo"); assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo")); } - #[test] - fn test_lowercase_ext() { - let t = dummy_context(); - let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap(); + #[async_std::test] + async fn test_lowercase_ext() { + let t = dummy_context().await; + let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello") + .await + .unwrap(); assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt"); } - #[test] - fn test_as_file_name() { - let t = dummy_context(); - let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); + #[async_std::test] + async fn test_as_file_name() { + let t = dummy_context().await; + let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello") + .await + .unwrap(); assert_eq!(blob.as_file_name(), "foo.txt"); } - #[test] - fn test_as_rel_path() { - let t = dummy_context(); - let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); + #[async_std::test] + async fn test_as_rel_path() { + let t = dummy_context().await; + let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello") + .await + .unwrap(); assert_eq!(blob.as_rel_path(), Path::new("foo.txt")); } - #[test] - fn test_suffix() { - let t = dummy_context(); - let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); + #[async_std::test] + async fn test_suffix() { + let t = dummy_context().await; + let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello") + .await + .unwrap(); assert_eq!(blob.suffix(), Some("txt")); - let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap(); + let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap(); assert_eq!(blob.suffix(), None); } - #[test] - fn test_create_dup() { - let t = dummy_context(); - BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); + #[async_std::test] + async fn test_create_dup() { + let t = dummy_context().await; + BlobObject::create(&t.ctx, "foo.txt", b"hello") + .await + .unwrap(); let foo_path = t.ctx.get_blobdir().join("foo.txt"); - assert!(foo_path.exists()); - BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap(); - for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() { + assert!(foo_path.exists().await); + BlobObject::create(&t.ctx, "foo.txt", b"world") + .await + .unwrap(); + let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap(); + while let Some(dirent) = dir.next().await { let fname = dirent.unwrap().file_name(); if fname == foo_path.file_name().unwrap() { - assert_eq!(fs::read(&foo_path).unwrap(), b"hello"); + assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello"); } else { let name = fname.to_str().unwrap(); assert!(name.starts_with("foo")); @@ -478,17 +502,22 @@ mod tests { } } - #[test] - fn test_double_ext_preserved() { - let t = dummy_context(); - BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap(); + #[async_std::test] + async fn test_double_ext_preserved() { + let t = dummy_context().await; + BlobObject::create(&t.ctx, "foo.tar.gz", b"hello") + .await + .unwrap(); let foo_path = t.ctx.get_blobdir().join("foo.tar.gz"); - assert!(foo_path.exists()); - BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap(); - for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() { + assert!(foo_path.exists().await); + BlobObject::create(&t.ctx, "foo.tar.gz", b"world") + .await + .unwrap(); + let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap(); + while let Some(dirent) = dir.next().await { let fname = dirent.unwrap().file_name(); if fname == foo_path.file_name().unwrap() { - assert_eq!(fs::read(&foo_path).unwrap(), b"hello"); + assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello"); } else { let name = fname.to_str().unwrap(); println!("{}", name); @@ -498,55 +527,55 @@ mod tests { } } - #[test] - fn test_create_long_names() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_long_names() { + let t = dummy_context().await; let s = "1".repeat(150); - let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap(); + let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap(); let blobname = blob.as_name().split('/').last().unwrap(); assert!(blobname.len() < 128); } - #[test] - fn test_create_and_copy() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_and_copy() { + let t = dummy_context().await; let src = t.dir.path().join("src"); - fs::write(&src, b"boo").unwrap(); - let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap(); + fs::write(&src, b"boo").await.unwrap(); + let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap(); assert_eq!(blob.as_name(), "$BLOBDIR/src"); - let data = fs::read(blob.to_abs_path()).unwrap(); + let data = fs::read(blob.to_abs_path()).await.unwrap(); assert_eq!(data, b"boo"); let whoops = t.dir.path().join("whoops"); - assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err()); + assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err()); let whoops = t.ctx.get_blobdir().join("whoops"); - assert!(!whoops.exists()); + assert!(!whoops.exists().await); } - #[test] - fn test_create_from_path() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_from_path() { + let t = dummy_context().await; let src_ext = t.dir.path().join("external"); - fs::write(&src_ext, b"boo").unwrap(); - let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap(); + fs::write(&src_ext, b"boo").await.unwrap(); + let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap(); assert_eq!(blob.as_name(), "$BLOBDIR/external"); - let data = fs::read(blob.to_abs_path()).unwrap(); + let data = fs::read(blob.to_abs_path()).await.unwrap(); assert_eq!(data, b"boo"); let src_int = t.ctx.get_blobdir().join("internal"); - fs::write(&src_int, b"boo").unwrap(); - let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap(); + fs::write(&src_int, b"boo").await.unwrap(); + let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap(); assert_eq!(blob.as_name(), "$BLOBDIR/internal"); - let data = fs::read(blob.to_abs_path()).unwrap(); + let data = fs::read(blob.to_abs_path()).await.unwrap(); assert_eq!(data, b"boo"); } - #[test] - fn test_create_from_name_long() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_from_name_long() { + let t = dummy_context().await; let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html"); - fs::write(&src_ext, b"boo").unwrap(); - let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap(); + fs::write(&src_ext, b"boo").await.unwrap(); + let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap(); assert_eq!( blob.as_name(), "$BLOBDIR/autocrypt-setup-message-4137848473.html" diff --git a/src/chat.rs b/src/chat.rs index 5c409dbbe..ae1efd74f 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,9 +1,9 @@ //! # Chat module use std::convert::TryFrom; -use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; +use async_std::path::{Path, PathBuf}; use itertools::Itertools; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; @@ -17,7 +17,7 @@ use crate::context::Context; use crate::dc_tools::*; use crate::error::{bail, ensure, format_err, Error}; use crate::events::Event; -use crate::job::*; +use crate::job::{self, Action}; use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::*; @@ -108,36 +108,44 @@ impl ChatId { self.0 == DC_CHAT_ID_ALLDONE_HINT } - pub fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<(), Error> { - context.sql.execute( - "UPDATE contacts + pub async fn set_selfavatar_timestamp( + self, + context: &Context, + timestamp: i64, + ) -> Result<(), Error> { + context + .sql + .execute( + "UPDATE contacts SET selfavatar_sent=? WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);", - params![timestamp, self], - )?; + paramsv![timestamp, self], + ) + .await?; Ok(()) } - pub fn set_blocked(self, context: &Context, new_blocked: Blocked) -> bool { + pub async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> bool { if self.is_special() { warn!(context, "ignoring setting of Block-status for {}", self); return false; } - sql::execute( - context, - &context.sql, - "UPDATE chats SET blocked=? WHERE id=?;", - params![new_blocked, self], - ) - .is_ok() + context + .sql + .execute( + "UPDATE chats SET blocked=? WHERE id=?;", + paramsv![new_blocked, self], + ) + .await + .is_ok() } - pub fn unblock(self, context: &Context) { - self.set_blocked(context, Blocked::Not); + pub async fn unblock(self, context: &Context) { + self.set_blocked(context, Blocked::Not).await; } /// Archives or unarchives a chat. - pub fn set_visibility( + pub async fn set_visibility( self, context: &Context, visibility: ChatVisibility, @@ -149,22 +157,24 @@ impl ChatId { ); if visibility == ChatVisibility::Archived { - sql::execute( - context, - &context.sql, - "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;", - params![MessageState::InNoticed, self, MessageState::InFresh], - )?; + context + .sql + .execute( + "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;", + paramsv![MessageState::InNoticed, self, MessageState::InFresh], + ) + .await?; } - sql::execute( - context, - &context.sql, - "UPDATE chats SET archived=? WHERE id=?;", - params![visibility, self], - )?; + context + .sql + .execute( + "UPDATE chats SET archived=? WHERE id=?;", + paramsv![visibility, self], + ) + .await?; - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }); @@ -174,18 +184,19 @@ impl ChatId { // note that unarchive() is not the same as set_visibility(Normal) - // eg. unarchive() does not modify pinned chats and does not send events. - pub fn unarchive(self, context: &Context) -> Result<(), Error> { - sql::execute( - context, - &context.sql, - "UPDATE chats SET archived=0 WHERE id=? and archived=1", - params![self], - )?; + pub async fn unarchive(self, context: &Context) -> Result<(), Error> { + context + .sql + .execute( + "UPDATE chats SET archived=0 WHERE id=? and archived=1", + paramsv![self], + ) + .await?; Ok(()) } /// Deletes a chat. - pub fn delete(self, context: &Context) -> Result<(), Error> { + pub async fn delete(self, context: &Context) -> Result<(), Error> { ensure!( !self.is_special(), "bad chat_id, can not be a special chat: {}", @@ -193,42 +204,41 @@ impl ChatId { ); /* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */ - let _chat = Chat::load_from_db(context, self)?; - sql::execute( - context, - &context.sql, - "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", - params![self], - )?; + let _chat = Chat::load_from_db(context, self).await?; + context + .sql + .execute( + "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", + paramsv![self], + ) + .await?; - sql::execute( - context, - &context.sql, - "DELETE FROM msgs WHERE chat_id=?;", - params![self], - )?; + context + .sql + .execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self]) + .await?; - sql::execute( - context, - &context.sql, - "DELETE FROM chats_contacts WHERE chat_id=?;", - params![self], - )?; + context + .sql + .execute( + "DELETE FROM chats_contacts WHERE chat_id=?;", + paramsv![self], + ) + .await?; - sql::execute( - context, - &context.sql, - "DELETE FROM chats WHERE id=?;", - params![self], - )?; + context + .sql + .execute("DELETE FROM chats WHERE id=?;", paramsv![self]) + .await?; - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }); - job_kill_action(context, Action::Housekeeping); - job_add(context, Action::Housekeeping, 0, Params::new(), 10); + job::kill_action(context, Action::Housekeeping).await; + let j = job::Job::new(Action::Housekeeping, 0, Params::new(), 10); + job::add(context, j).await; Ok(()) } @@ -236,18 +246,18 @@ impl ChatId { /// Sets draft message. /// /// Passing `None` as message just deletes the draft - pub fn set_draft(self, context: &Context, msg: Option<&mut Message>) { + pub async fn set_draft(self, context: &Context, msg: Option<&mut Message>) { if self.is_special() { return; } let changed = match msg { - None => self.maybe_delete_draft(context), - Some(msg) => self.set_draft_raw(context, msg), + None => self.maybe_delete_draft(context).await, + Some(msg) => self.set_draft_raw(context, msg).await, }; if changed { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: self, msg_id: MsgId::new(0), }); @@ -255,28 +265,34 @@ impl ChatId { } // similar to as dc_set_draft() but does not emit an event - fn set_draft_raw(self, context: &Context, msg: &mut Message) -> bool { - let deleted = self.maybe_delete_draft(context); - let set = self.do_set_draft(context, msg).is_ok(); + async fn set_draft_raw(self, context: &Context, msg: &mut Message) -> bool { + let deleted = self.maybe_delete_draft(context).await; + let set = self.do_set_draft(context, msg).await.is_ok(); // Can't inline. Both functions above must be called, no shortcut! deleted || set } - fn get_draft_msg_id(self, context: &Context) -> Option { - context.sql.query_get_value::<_, MsgId>( - context, - "SELECT id FROM msgs WHERE chat_id=? AND state=?;", - params![self, MessageState::OutDraft], - ) + async fn get_draft_msg_id(self, context: &Context) -> Option { + context + .sql + .query_get_value::( + context, + "SELECT id FROM msgs WHERE chat_id=? AND state=?;", + paramsv![self, MessageState::OutDraft], + ) + .await } - pub fn get_draft(self, context: &Context) -> Result, Error> { + pub async fn get_draft(self, context: &Context) -> Result, Error> { if self.is_special() { return Ok(None); } - match self.get_draft_msg_id(context) { - Some(draft_msg_id) => Ok(Some(Message::load_from_db(context, draft_msg_id)?)), + match self.get_draft_msg_id(context).await { + Some(draft_msg_id) => { + let msg = Message::load_from_db(context, draft_msg_id).await?; + Ok(Some(msg)) + } None => Ok(None), } } @@ -284,9 +300,9 @@ impl ChatId { /// Delete draft message in specified chat, if there is one. /// /// Returns `true`, if message was deleted, `false` otherwise. - fn maybe_delete_draft(self, context: &Context) -> bool { - match self.get_draft_msg_id(context) { - Some(msg_id) => msg_id.delete_from_db(context).is_ok(), + async fn maybe_delete_draft(self, context: &Context) -> bool { + match self.get_draft_msg_id(context).await { + Some(msg_id) => msg_id.delete_from_db(context).await.is_ok(), None => false, } } @@ -294,7 +310,7 @@ impl ChatId { /// Set provided message as draft message for specified chat. /// /// Return true on success, false on database error. - fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<(), Error> { + async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<(), Error> { match msg.viewtype { Viewtype::Unknown => bail!("Can not set draft of unknown type."), Viewtype::Text => match msg.text.as_ref() { @@ -308,77 +324,87 @@ impl ChatId { _ => { let blob = msg .param - .get_blob(Param::File, context, !msg.is_increation())? + .get_blob(Param::File, context, !msg.is_increation()) + .await? .ok_or_else(|| format_err!("No file stored in params"))?; msg.param.set(Param::File, blob.as_name()); } } - sql::execute( - context, - &context.sql, - "INSERT INTO msgs (chat_id, from_id, timestamp, type, state, txt, param, hidden) + context + .sql + .execute( + "INSERT INTO msgs (chat_id, from_id, timestamp, type, state, txt, param, hidden) VALUES (?,?,?, ?,?,?,?,?);", - params![ - self, - DC_CONTACT_ID_SELF, - time(), - msg.viewtype, - MessageState::OutDraft, - msg.text.as_deref().unwrap_or(""), - msg.param.to_string(), - 1, - ], - )?; + paramsv![ + self, + DC_CONTACT_ID_SELF, + time(), + msg.viewtype, + MessageState::OutDraft, + msg.text.as_deref().unwrap_or(""), + msg.param.to_string(), + 1, + ], + ) + .await?; Ok(()) } /// Returns number of messages in a chat. - pub fn get_msg_cnt(self, context: &Context) -> usize { + pub async fn get_msg_cnt(self, context: &Context) -> usize { context .sql - .query_get_value::<_, i32>( + .query_get_value::( context, "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", - params![self], + paramsv![self], ) + .await .unwrap_or_default() as usize } - pub fn get_fresh_msg_cnt(self, context: &Context) -> usize { + pub async fn get_fresh_msg_cnt(self, context: &Context) -> usize { context .sql - .query_get_value::<_, i32>( + .query_get_value::( context, "SELECT COUNT(*) - FROM msgs - WHERE state=10 + FROM msgs + WHERE state=10 AND hidden=0 AND chat_id=?;", - params![self], + paramsv![self], ) + .await .unwrap_or_default() as usize } - pub(crate) fn get_param(self, context: &Context) -> Result { + pub(crate) async fn get_param(self, context: &Context) -> Result { let res: Option = context .sql - .query_get_value_result("SELECT param FROM chats WHERE id=?", params![self])?; + .query_get_value_result("SELECT param FROM chats WHERE id=?", paramsv![self]) + .await?; Ok(res .map(|s| s.parse().unwrap_or_default()) .unwrap_or_default()) } // Returns true if chat is a saved messages chat. - pub fn is_self_talk(self, context: &Context) -> Result { - Ok(self.get_param(context)?.exists(Param::Selftalk)) + pub async fn is_self_talk(self, context: &Context) -> Result { + Ok(self.get_param(context).await?.exists(Param::Selftalk)) } /// Returns true if chat is a device chat. - pub fn is_device_talk(self, context: &Context) -> Result { - Ok(self.get_param(context)?.exists(Param::Devicetalk)) + pub async fn is_device_talk(self, context: &Context) -> Result { + Ok(self.get_param(context).await?.exists(Param::Devicetalk)) } - fn parent_query(self, context: &Context, fields: &str, f: F) -> sql::Result> + async fn parent_query( + self, + context: &Context, + fields: &str, + f: F, + ) -> sql::Result> where F: FnOnce(&rusqlite::Row) -> rusqlite::Result, { @@ -392,7 +418,7 @@ impl ChatId { ); sql.query_row_optional( query, - params![ + paramsv![ self, MessageState::OutPreparing, MessageState::OutDraft, @@ -401,22 +427,24 @@ impl ChatId { ], f, ) + .await } - fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> { + async fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> { let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)); self.parent_query( context, "rfc724_mid, mime_in_reply_to, mime_references", collect, ) + .await .ok() .flatten() } - fn parent_is_encrypted(self, context: &Context) -> Result { + async fn parent_is_encrypted(self, context: &Context) -> Result { let collect = |row: &rusqlite::Row| Ok(row.get(0)?); - let packed: Option = self.parent_query(context, "param", collect)?; + let packed: Option = self.parent_query(context, "param", collect).await?; if let Some(ref packed) = packed { let param = packed.parse::()?; @@ -500,28 +528,31 @@ pub struct Chat { impl Chat { /// Loads chat from the database by its ID. - pub fn load_from_db(context: &Context, chat_id: ChatId) -> Result { - let res = context.sql.query_row( - "SELECT c.type, c.name, c.grpid, c.param, c.archived, + pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result { + let res = context + .sql + .query_row( + "SELECT c.type, c.name, c.grpid, c.param, c.archived, c.blocked, c.locations_send_until, c.muted_until FROM chats c WHERE c.id=?;", - params![chat_id], - |row| { - let c = Chat { - id: chat_id, - typ: row.get(0)?, - name: row.get::<_, String>(1)?, - grpid: row.get::<_, String>(2)?, - param: row.get::<_, String>(3)?.parse().unwrap_or_default(), - visibility: row.get(4)?, - blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), - is_sending_locations: row.get(6)?, - mute_duration: row.get(7)?, - }; - Ok(c) - }, - ); + paramsv![chat_id], + |row| { + let c = Chat { + id: chat_id, + typ: row.get(0)?, + name: row.get::<_, String>(1)?, + grpid: row.get::<_, String>(2)?, + param: row.get::<_, String>(3)?.parse().unwrap_or_default(), + visibility: row.get(4)?, + blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), + is_sending_locations: row.get(6)?, + mute_duration: row.get(7)?, + }; + Ok(c) + }, + ) + .await; match res { Err(err @ crate::sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { @@ -536,28 +567,28 @@ impl Chat { } Ok(mut chat) => { if chat.id.is_deaddrop() { - chat.name = context.stock_str(StockMessage::DeadDrop).into(); + chat.name = context.stock_str(StockMessage::DeadDrop).await.into(); } else if chat.id.is_archived_link() { - let tempname = context.stock_str(StockMessage::ArchivedChats); - let cnt = dc_get_archived_cnt(context); + let tempname = context.stock_str(StockMessage::ArchivedChats).await; + let cnt = dc_get_archived_cnt(context).await; chat.name = format!("{} ({})", tempname, cnt); } else if chat.id.is_starred() { - chat.name = context.stock_str(StockMessage::StarredMsgs).into(); + chat.name = context.stock_str(StockMessage::StarredMsgs).await.into(); } else { if chat.typ == Chattype::Single { - let contacts = get_chat_contacts(context, chat.id); + let contacts = get_chat_contacts(context, chat.id).await; let mut chat_name = "Err [Name not found]".to_owned(); if let Some(contact_id) = contacts.first() { - if let Ok(contact) = Contact::get_by_id(context, *contact_id) { + if let Ok(contact) = Contact::get_by_id(context, *contact_id).await { chat_name = contact.get_display_name().to_owned(); } } chat.name = chat_name; } if chat.param.exists(Param::Selftalk) { - chat.name = context.stock_str(StockMessage::SavedMessages).into(); + chat.name = context.stock_str(StockMessage::SavedMessages).await.into(); } else if chat.param.exists(Param::Devicetalk) { - chat.name = context.stock_str(StockMessage::DeviceMessages).into(); + chat.name = context.stock_str(StockMessage::DeviceMessages).await.into(); } } Ok(chat) @@ -579,13 +610,14 @@ impl Chat { !self.id.is_special() && !self.is_device_talk() } - pub fn update_param(&mut self, context: &Context) -> Result<(), Error> { - sql::execute( - context, - &context.sql, - "UPDATE chats SET param=? WHERE id=?", - params![self.param.to_string(), self.id], - )?; + pub async fn update_param(&mut self, context: &Context) -> Result<(), Error> { + context + .sql + .execute( + "UPDATE chats SET param=? WHERE id=?", + paramsv![self.param.to_string(), self.id], + ) + .await?; Ok(()) } @@ -604,16 +636,16 @@ impl Chat { &self.name } - pub fn get_profile_image(&self, context: &Context) -> Option { + pub async fn get_profile_image(&self, context: &Context) -> Option { if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { return Some(dc_get_abs_path(context, image_rel)); } } else if self.typ == Chattype::Single { - let contacts = get_chat_contacts(context, self.id); + let contacts = get_chat_contacts(context, self.id).await; if let Some(contact_id) = contacts.first() { - if let Ok(contact) = Contact::get_by_id(context, *contact_id) { - return contact.get_profile_image(context); + if let Ok(contact) = Contact::get_by_id(context, *contact_id).await { + return contact.get_profile_image(context).await; } } } @@ -621,17 +653,17 @@ impl Chat { None } - pub fn get_gossiped_timestamp(&self, context: &Context) -> i64 { - get_gossiped_timestamp(context, self.id) + pub async fn get_gossiped_timestamp(&self, context: &Context) -> i64 { + get_gossiped_timestamp(context, self.id).await } - pub fn get_color(&self, context: &Context) -> u32 { + pub async fn get_color(&self, context: &Context) -> u32 { let mut color = 0; if self.typ == Chattype::Single { - let contacts = get_chat_contacts(context, self.id); + let contacts = get_chat_contacts(context, self.id).await; if let Some(contact_id) = contacts.first() { - if let Ok(contact) = Contact::get_by_id(context, *contact_id) { + if let Ok(contact) = Contact::get_by_id(context, *contact_id).await { color = contact.get_color(); } } @@ -646,8 +678,8 @@ impl Chat { /// /// This is somewhat experimental, even more so than the rest of /// deltachat, and the data returned is still subject to change. - pub fn get_info(&self, context: &Context) -> Result { - let draft = match self.id.get_draft(context)? { + pub async fn get_info(&self, context: &Context) -> Result { + let draft = match self.id.get_draft(context).await? { Some(message) => message.text.unwrap_or_else(String::new), _ => String::new(), }; @@ -657,10 +689,14 @@ impl Chat { name: self.name.clone(), archived: self.visibility == ChatVisibility::Archived, param: self.param.to_string(), - gossiped_timestamp: self.get_gossiped_timestamp(context), + gossiped_timestamp: self.get_gossiped_timestamp(context).await, is_sending_locations: self.is_sending_locations, - color: self.get_color(context), - profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new), + color: self.get_color(context).await, + profile_image: self + .get_profile_image(context) + .await + .map(Into::into) + .unwrap_or_else(std::path::PathBuf::new), draft, is_muted: self.is_muted(), }) @@ -696,7 +732,7 @@ impl Chat { } } - fn prepare_msg_raw( + async fn prepare_msg_raw( &mut self, context: &Context, msg: &mut Message, @@ -717,7 +753,7 @@ impl Chat { } if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup) - && !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF) + && !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await { emit_event!( context, @@ -726,7 +762,7 @@ impl Chat { bail!("Cannot set message; self not in group."); } - if let Some(from) = context.get_config(Config::ConfiguredAddr) { + if let Some(from) = context.get_config(Config::ConfiguredAddr).await { let new_rfc724_mid = { let grpid = match self.typ { Chattype::Group | Chattype::VerifiedGroup => Some(self.grpid.as_str()), @@ -736,11 +772,15 @@ impl Chat { }; if self.typ == Chattype::Single { - if let Some(id) = context.sql.query_get_value( - context, - "SELECT contact_id FROM chats_contacts WHERE chat_id=?;", - params![self.id], - ) { + if let Some(id) = context + .sql + .query_get_value( + context, + "SELECT contact_id FROM chats_contacts WHERE chat_id=?;", + paramsv![self.id], + ) + .await + { to_id = id; } else { error!( @@ -754,7 +794,7 @@ impl Chat { { msg.param.set_int(Param::AttachGroupImage, 1); self.param.remove(Param::Unpromoted); - self.update_param(context)?; + self.update_param(context).await?; } /* check if we want to encrypt this message. If yes and circumstances change @@ -762,46 +802,49 @@ impl Chat { we might not send the message out at all */ if msg.param.get_int(Param::ForcePlaintext).unwrap_or_default() == 0 { let mut can_encrypt = true; - let mut all_mutual = context.get_config_bool(Config::E2eeEnabled); + let mut all_mutual = context.get_config_bool(Config::E2eeEnabled).await; // take care that this statement returns NULL rows // if there is no peerstates for a chat member! // for DC_PARAM_SELFTALK this statement does not return any row - let res = context.sql.query_map( - "SELECT ps.prefer_encrypted, c.addr \ + let res = context + .sql + .query_map( + "SELECT ps.prefer_encrypted, c.addr \ FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE cc.chat_id=? AND cc.contact_id>9;", - params![self.id], - |row| { - let addr: String = row.get(1)?; + paramsv![self.id], + |row| { + let addr: String = row.get(1)?; - if let Some(prefer_encrypted) = row.get::<_, Option>(0)? { - // the peerstate exist, so we have either public_key or gossip_key - // and can encrypt potentially - if prefer_encrypted != 1 { - info!( - context, - "[autocrypt] peerstate for {} is {}", - addr, - if prefer_encrypted == 0 { - "NOPREFERENCE" - } else { - "RESET" - }, - ); + if let Some(prefer_encrypted) = row.get::<_, Option>(0)? { + // the peerstate exist, so we have either public_key or gossip_key + // and can encrypt potentially + if prefer_encrypted != 1 { + info!( + context, + "[autocrypt] peerstate for {} is {}", + addr, + if prefer_encrypted == 0 { + "NOPREFERENCE" + } else { + "RESET" + }, + ); + all_mutual = false; + } + } else { + info!(context, "[autocrypt] no peerstate for {}", addr,); + can_encrypt = false; all_mutual = false; } - } else { - info!(context, "[autocrypt] no peerstate for {}", addr,); - can_encrypt = false; - all_mutual = false; - } - Ok(()) - }, - |rows| rows.collect::, _>>().map_err(Into::into), - ); + Ok(()) + }, + |rows| rows.collect::, _>>().map_err(Into::into), + ) + .await; match res { Ok(_) => {} Err(err) => { @@ -809,7 +852,7 @@ impl Chat { } } - if can_encrypt && (all_mutual || self.id.parent_is_encrypted(context)?) { + if can_encrypt && (all_mutual || self.id.parent_is_encrypted(context).await?) { msg.param.set_int(Param::GuaranteeE2ee, 1); } } @@ -824,7 +867,7 @@ impl Chat { // we do not set In-Reply-To/References in this case. if !self.is_self_talk() { if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) = - self.id.get_parent_mime_headers(context) + self.id.get_parent_mime_headers(context).await { if !parent_rfc724_mid.is_empty() { new_in_reply_to = parent_rfc724_mid.clone(); @@ -854,40 +897,41 @@ impl Chat { // add independent location to database if msg.param.exists(Param::SetLatitude) - && sql::execute( - context, - &context.sql, - "INSERT INTO locations \ + && context + .sql + .execute( + "INSERT INTO locations \ (timestamp,from_id,chat_id, latitude,longitude,independent)\ VALUES (?,?,?, ?,?,1);", // 1=DC_CONTACT_ID_SELF - params![ - timestamp, - DC_CONTACT_ID_SELF, - self.id, - msg.param.get_float(Param::SetLatitude).unwrap_or_default(), - msg.param.get_float(Param::SetLongitude).unwrap_or_default(), - ], - ) - .is_ok() + paramsv![ + timestamp, + DC_CONTACT_ID_SELF, + self.id, + msg.param.get_float(Param::SetLatitude).unwrap_or_default(), + msg.param.get_float(Param::SetLongitude).unwrap_or_default(), + ], + ) + .await + .is_ok() { - location_id = sql::get_rowid2( - context, - &context.sql, - "locations", - "timestamp", - timestamp, - "from_id", - DC_CONTACT_ID_SELF as i32, - ); + location_id = context + .sql + .get_rowid2( + context, + "locations", + "timestamp", + timestamp, + "from_id", + DC_CONTACT_ID_SELF as i32, + ) + .await?; } // add message to the database - if sql::execute( - context, - &context.sql, + if context.sql.execute( "INSERT INTO msgs (rfc724_mid, chat_id, from_id, to_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to, mime_references, location_id) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?);", - params![ + paramsv![ new_rfc724_mid, self.id, DC_CONTACT_ID_SELF, @@ -895,21 +939,20 @@ impl Chat { timestamp, msg.viewtype, msg.state, - msg.text.as_ref().map_or("", String::as_str), + msg.text.as_ref().cloned().unwrap_or_default(), msg.param.to_string(), msg.hidden, new_in_reply_to, new_references, location_id as i32, ] - ).is_ok() { - msg_id = sql::get_rowid( + ).await.is_ok() { + msg_id = context.sql.get_rowid( context, - &context.sql, "msgs", "rfc724_mid", new_rfc724_mid, - ); + ).await?; } else { error!( context, @@ -1001,7 +1044,7 @@ pub struct ChatInfo { /// /// If there is no profile image set this will be an empty string /// currently. - pub profile_image: PathBuf, + pub profile_image: std::path::PathBuf, /// The draft message text. /// @@ -1045,23 +1088,23 @@ pub struct ChatInfo { /// # Returns /// /// The "created" chat ID is returned. -pub fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result { - let msg = Message::load_from_db(context, msg_id)?; - let chat = Chat::load_from_db(context, msg.chat_id)?; +pub async fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result { + let msg = Message::load_from_db(context, msg_id).await?; + let chat = Chat::load_from_db(context, msg.chat_id).await?; ensure!( !chat.id.is_special(), "Message can not belong to a special chat" ); if chat.blocked != Blocked::Not { - chat.id.unblock(context); + chat.id.unblock(context).await; // Sending with 0s as data since multiple messages may have changed. - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); } - Contact::scaleup_origin_by_id(context, msg.from_id, Origin::CreateChat); + Contact::scaleup_origin_by_id(context, msg.from_id, Origin::CreateChat).await; Ok(chat.id) } @@ -1071,17 +1114,18 @@ pub fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result Result { - let chat_id = match lookup_by_contact_id(context, contact_id) { +pub async fn create_by_contact_id(context: &Context, contact_id: u32) -> Result { + let chat_id = match lookup_by_contact_id(context, contact_id).await { Ok((chat_id, chat_blocked)) => { if chat_blocked != Blocked::Not { // unblock chat (typically move it from the deaddrop to view - chat_id.unblock(context); + chat_id.unblock(context).await; } chat_id } Err(err) => { - if !Contact::real_exists_by_id(context, contact_id) && contact_id != DC_CONTACT_ID_SELF + if !Contact::real_exists_by_id(context, contact_id).await + && contact_id != DC_CONTACT_ID_SELF { warn!( context, @@ -1090,14 +1134,14 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result Result Result<(), Error> { +pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<(), Error> { // if there is no saved-messages chat, there is nothing to update. this is no error. - if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) { + if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF).await { let icon = include_bytes!("../assets/icon-saved-messages.png"); - let blob = BlobObject::create(context, "icon-saved-messages.png".to_string(), icon)?; + let blob = BlobObject::create(context, "icon-saved-messages.png".to_string(), icon).await?; let icon = blob.as_name().to_string(); - let mut chat = Chat::load_from_db(context, chat_id)?; + let mut chat = Chat::load_from_db(context, chat_id).await?; chat.param.set(Param::ProfileImage, icon); - chat.update_param(context)?; + chat.update_param(context).await?; } Ok(()) } -pub(crate) fn update_device_icon(context: &Context) -> Result<(), Error> { +pub(crate) async fn update_device_icon(context: &Context) -> Result<(), Error> { // if there is no device-chat, there is nothing to update. this is no error. - if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) { + if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE).await { let icon = include_bytes!("../assets/icon-device.png"); - let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?; + let blob = BlobObject::create(context, "icon-device.png".to_string(), icon).await?; let icon = blob.as_name().to_string(); - let mut chat = Chat::load_from_db(context, chat_id)?; + let mut chat = Chat::load_from_db(context, chat_id).await?; chat.param.set(Param::ProfileImage, &icon); - chat.update_param(context)?; + chat.update_param(context).await?; - let mut contact = Contact::load_from_db(context, DC_CONTACT_ID_DEVICE)?; + let mut contact = Contact::load_from_db(context, DC_CONTACT_ID_DEVICE).await?; contact.param.set(Param::ProfileImage, icon); - contact.update_param(context)?; + contact.update_param(context).await?; } Ok(()) } -fn update_special_chat_name( +async fn update_special_chat_name( context: &Context, contact_id: u32, stock_id: StockMessage, ) -> Result<(), Error> { - if let Ok((chat_id, _)) = lookup_by_contact_id(context, contact_id) { - let name: String = context.stock_str(stock_id).into(); + if let Ok((chat_id, _)) = lookup_by_contact_id(context, contact_id).await { + let name: String = context.stock_str(stock_id).await.into(); // the `!= name` condition avoids unneeded writes - context.sql.execute( - "UPDATE chats SET name=? WHERE id=? AND name!=?;", - params![name, chat_id, name], - )?; + context + .sql + .execute( + "UPDATE chats SET name=? WHERE id=? AND name!=?;", + paramsv![name, chat_id, name], + ) + .await?; } Ok(()) } -pub(crate) fn update_special_chat_names(context: &Context) -> Result<(), Error> { - update_special_chat_name(context, DC_CONTACT_ID_DEVICE, StockMessage::DeviceMessages)?; - update_special_chat_name(context, DC_CONTACT_ID_SELF, StockMessage::SavedMessages)?; +pub(crate) async fn update_special_chat_names(context: &Context) -> Result<(), Error> { + update_special_chat_name(context, DC_CONTACT_ID_DEVICE, StockMessage::DeviceMessages).await?; + update_special_chat_name(context, DC_CONTACT_ID_SELF, StockMessage::SavedMessages).await?; Ok(()) } -pub(crate) fn create_or_lookup_by_contact_id( +pub(crate) async fn create_or_lookup_by_contact_id( context: &Context, contact_id: u32, create_blocked: Blocked, ) -> Result<(ChatId, Blocked), Error> { - ensure!(context.sql.is_open(), "Database not available"); + ensure!(context.sql.is_open().await, "Database not available"); ensure!(contact_id > 0, "Invalid contact id requested"); - if let Ok((chat_id, chat_blocked)) = lookup_by_contact_id(context, contact_id) { + if let Ok((chat_id, chat_blocked)) = lookup_by_contact_id(context, contact_id).await { // Already exists, no need to create. return Ok((chat_id, chat_blocked)); } - let contact = Contact::load_from_db(context, contact_id)?; - let chat_name = contact.get_display_name(); + let contact = Contact::load_from_db(context, contact_id).await?; + let chat_name = contact.get_display_name().to_string(); context .sql - .start_stmt("create_or_lookup_by_contact_id transaction"); - context.sql.with_conn(|conn| { - let tx = conn.transaction()?; - tx.execute( - "INSERT INTO chats (type, name, param, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?)", - params![ - Chattype::Single, - chat_name, - match contact_id { - DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk - DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk - _ => "".to_string(), - }, - create_blocked as u8, - time(), - ] - )?; - tx.execute( - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)", - params![contact_id])?; - tx.commit()?; - Ok(()) - })?; + .with_conn(move |mut conn| { + let conn2 = &mut conn; + let tx = conn2.transaction()?; + tx.execute( + "INSERT INTO chats (type, name, param, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?)", + params![ + Chattype::Single, + chat_name, + match contact_id { + DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk + DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk + _ => "".to_string(), + }, + create_blocked as u8, + time(), + ], + )?; + + tx.execute( + "INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)", + params![contact_id], + )?; + + tx.commit()?; + Ok(()) + }) + .await?; if contact_id == DC_CONTACT_ID_SELF { - update_saved_messages_icon(context)?; + update_saved_messages_icon(context).await?; } else if contact_id == DC_CONTACT_ID_DEVICE { - update_device_icon(context)?; + update_device_icon(context).await?; } - lookup_by_contact_id(context, contact_id) + lookup_by_contact_id(context, contact_id).await } -pub(crate) fn lookup_by_contact_id( +pub(crate) async fn lookup_by_contact_id( context: &Context, contact_id: u32, ) -> Result<(ChatId, Blocked), Error> { - ensure!(context.sql.is_open(), "Database not available"); + ensure!(context.sql.is_open().await, "Database not available"); context .sql @@ -1226,7 +1277,7 @@ pub(crate) fn lookup_by_contact_id( WHERE c.type=100 AND c.id>9 AND j.contact_id=?;", - params![contact_id as i32], + paramsv![contact_id as i32], |row| { Ok(( row.get::<_, ChatId>(0)?, @@ -1234,18 +1285,19 @@ pub(crate) fn lookup_by_contact_id( )) }, ) + .await .map_err(Into::into) } -pub fn get_by_contact_id(context: &Context, contact_id: u32) -> Result { - let (chat_id, blocked) = lookup_by_contact_id(context, contact_id)?; +pub async fn get_by_contact_id(context: &Context, contact_id: u32) -> Result { + let (chat_id, blocked) = lookup_by_contact_id(context, contact_id).await?; ensure_eq!(blocked, Blocked::Not, "Requested contact is blocked"); Ok(chat_id) } -pub fn prepare_msg<'a>( - context: &'a Context, +pub async fn prepare_msg( + context: &Context, chat_id: ChatId, msg: &mut Message, ) -> Result { @@ -1255,8 +1307,8 @@ pub fn prepare_msg<'a>( ); msg.state = MessageState::OutPreparing; - let msg_id = prepare_msg_common(context, chat_id, msg)?; - context.call_cb(Event::MsgsChanged { + let msg_id = prepare_msg_common(context, chat_id, msg).await?; + context.emit_event(Event::MsgsChanged { chat_id: msg.chat_id, msg_id: msg.id, }); @@ -1278,13 +1330,14 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool { } } -fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> { +async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> { if msg.viewtype == Viewtype::Text { // the caller should check if the message text is empty } else if msgtype_has_file(msg.viewtype) { let blob = msg .param - .get_blob(Param::File, context, !msg.is_increation())? + .get_blob(Param::File, context, !msg.is_increation()) + .await? .ok_or_else(|| { format_err!("Attachment missing for message of type #{}", msg.viewtype) })?; @@ -1320,16 +1373,16 @@ fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> { Ok(()) } -fn prepare_msg_common( +async fn prepare_msg_common( context: &Context, chat_id: ChatId, msg: &mut Message, ) -> Result { msg.id = MsgId::new_unset(); - prepare_msg_blob(context, msg)?; - chat_id.unarchive(context)?; + prepare_msg_blob(context, msg).await?; + chat_id.unarchive(context).await?; - let mut chat = Chat::load_from_db(context, chat_id)?; + let mut chat = Chat::load_from_db(context, chat_id).await?; ensure!(chat.can_send(), "cannot send to {}", chat_id); // The OutPreparing state is set by dc_prepare_msg() before it @@ -1340,23 +1393,28 @@ fn prepare_msg_common( msg.state = MessageState::OutPending; } - msg.id = chat.prepare_msg_raw(context, msg, dc_create_smeared_timestamp(context))?; + msg.id = chat + .prepare_msg_raw(context, msg, dc_create_smeared_timestamp(context).await) + .await?; msg.chat_id = chat_id; Ok(msg.id) } /// Returns whether a contact is in a chat or not. -pub fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool { - /* this function works for group and for normal chats, however, it is more useful for group chats. - DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group chat (DC_CONTACT_ID_SELF is not added to normal chats) */ +pub async fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool { + // this function works for group and for normal chats, however, it is more useful + // for group chats. + // DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group + // chat (DC_CONTACT_ID_SELF is not added to normal chats) context .sql .exists( "SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id=?;", - params![chat_id, contact_id as i32], + paramsv![chat_id, contact_id as i32], ) + .await .unwrap_or_default() } @@ -1369,34 +1427,11 @@ pub fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: u32) - // TODO: Do not allow ChatId to be 0, if prepare_msg had been called // the caller can get it from msg.chat_id. Forwards would need to // be fixed for this somehow too. -pub fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result { - // dc_prepare_msg() leaves the message state to OutPreparing, we - // only have to change the state to OutPending in this case. - // Otherwise we still have to prepare the message, which will set - // the state to OutPending. - if msg.state != MessageState::OutPreparing { - // automatically prepare normal messages - prepare_msg_common(context, chat_id, msg)?; - } else { - // update message state of separately prepared messages - ensure!( - chat_id.is_unset() || chat_id == msg.chat_id, - "Inconsistent chat ID" - ); - message::update_msg_state(context, msg.id, MessageState::OutPending); - } - - job_send_msg(context, msg.id)?; - - context.call_cb(Event::MsgsChanged { - chat_id: msg.chat_id, - msg_id: msg.id, - }); - - if msg.param.exists(Param::SetLatitude) { - context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); - } - +pub async fn send_msg( + context: &Context, + chat_id: ChatId, + msg: &mut Message, +) -> Result { if chat_id.is_unset() { let forwards = msg.param.get(Param::PrepForwards); if let Some(forwards) = forwards { @@ -1406,20 +1441,108 @@ pub fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result .map_err(|_| InvalidMsgId) .map(MsgId::new) { - if let Ok(mut msg) = Message::load_from_db(context, msg_id) { - send_msg(context, chat_id, &mut msg)?; + if let Ok(mut msg) = Message::load_from_db(context, msg_id).await { + send_msg_inner(context, chat_id, &mut msg).await?; }; } } msg.param.remove(Param::PrepForwards); - msg.save_param_to_disk(context); + msg.save_param_to_disk(context).await; + } + return send_msg_inner(context, chat_id, msg).await; + } + + send_msg_inner(context, chat_id, msg).await +} + +/// Tries to send a message synchronously. +/// +/// Directly opens an smtp +/// connection and sends the message, bypassing the job system. If this fails, it writes a send job to +/// the database. +pub async fn send_msg_sync( + context: &Context, + chat_id: ChatId, + msg: &mut Message, +) -> Result { + if context.is_io_running().await { + return send_msg(context, chat_id, msg).await; + } + + if let Some(mut job) = prepare_send_msg(context, chat_id, msg).await? { + let mut smtp = crate::smtp::Smtp::new(); + + let status = job.send_msg_to_smtp(context, &mut smtp).await; + + match status { + job::Status::Finished(Ok(_)) => { + context.emit_event(Event::MsgsChanged { + chat_id: msg.chat_id, + msg_id: msg.id, + }); + + Ok(msg.id) + } + _ => { + job.save(context).await?; + Err(format_err!( + "failed to send message, queued for later sending" + )) + } + } + } else { + // Nothing to do + Ok(msg.id) + } +} + +async fn send_msg_inner( + context: &Context, + chat_id: ChatId, + msg: &mut Message, +) -> Result { + if let Some(send_job) = prepare_send_msg(context, chat_id, msg).await? { + job::add(context, send_job).await; + + context.emit_event(Event::MsgsChanged { + chat_id: msg.chat_id, + msg_id: msg.id, + }); + + if msg.param.exists(Param::SetLatitude) { + context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); } } Ok(msg.id) } -pub fn send_text_msg( +async fn prepare_send_msg( + context: &Context, + chat_id: ChatId, + msg: &mut Message, +) -> Result, Error> { + // dc_prepare_msg() leaves the message state to OutPreparing, we + // only have to change the state to OutPending in this case. + // Otherwise we still have to prepare the message, which will set + // the state to OutPending. + if msg.state != MessageState::OutPreparing { + // automatically prepare normal messages + prepare_msg_common(context, chat_id, msg).await?; + } else { + // update message state of separately prepared messages + ensure!( + chat_id.is_unset() || chat_id == msg.chat_id, + "Inconsistent chat ID" + ); + message::update_msg_state(context, msg.id, MessageState::OutPending).await; + } + let job = job::send_msg_job(context, msg.id).await?; + + Ok(job) +} + +pub async fn send_text_msg( context: &Context, chat_id: ChatId, text_to_send: String, @@ -1432,20 +1555,20 @@ pub fn send_text_msg( let mut msg = Message::new(Viewtype::Text); msg.text = Some(text_to_send); - send_msg(context, chat_id, &mut msg) + send_msg(context, chat_id, &mut msg).await } -pub fn get_chat_msgs( +pub async fn get_chat_msgs( context: &Context, chat_id: ChatId, flags: u32, marker1before: Option, ) -> Vec { - match delete_device_expired_messages(context) { + match delete_device_expired_messages(context).await { Err(err) => warn!(context, "Failed to delete expired messages: {}", err), Ok(messages_deleted) => { if messages_deleted { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }) @@ -1479,10 +1602,12 @@ pub fn get_chat_msgs( Ok(ret) }; let success = if chat_id.is_deaddrop() { - let show_emails = - ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default(); - context.sql.query_map( - "SELECT m.id AS id, m.timestamp AS timestamp + let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await) + .unwrap_or_default(); + context + .sql + .query_map( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN chats ON m.chat_id=chats.id @@ -1495,13 +1620,16 @@ pub fn get_chat_msgs( AND contacts.blocked=0 AND m.msgrmsg>=? ORDER BY m.timestamp,m.id;", - params![if show_emails == ShowEmails::All { 0 } else { 1 }], - process_row, - process_rows, - ) + paramsv![if show_emails == ShowEmails::All { 0 } else { 1 }], + process_row, + process_rows, + ) + .await } else if chat_id.is_starred() { - context.sql.query_map( - "SELECT m.id AS id, m.timestamp AS timestamp + context + .sql + .query_map( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -1509,21 +1637,25 @@ pub fn get_chat_msgs( AND m.hidden=0 AND ct.blocked=0 ORDER BY m.timestamp,m.id;", - params![], - process_row, - process_rows, - ) + paramsv![], + process_row, + process_rows, + ) + .await } else { - context.sql.query_map( - "SELECT m.id AS id, m.timestamp AS timestamp + context + .sql + .query_map( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m WHERE m.chat_id=? AND m.hidden=0 ORDER BY m.timestamp, m.id;", - params![chat_id], - process_row, - process_rows, - ) + paramsv![chat_id], + process_row, + process_rows, + ) + .await }; match success { Ok(ret) => ret, @@ -1534,25 +1666,30 @@ pub fn get_chat_msgs( } } -pub fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> { - if !context.sql.exists( - "SELECT id FROM msgs WHERE chat_id=? AND state=?;", - params![chat_id, MessageState::InFresh], - )? { +pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> { + if !context + .sql + .exists( + "SELECT id FROM msgs WHERE chat_id=? AND state=?;", + paramsv![chat_id, MessageState::InFresh], + ) + .await? + { return Ok(()); } - sql::execute( - context, - &context.sql, - "UPDATE msgs + context + .sql + .execute( + "UPDATE msgs SET state=13 WHERE chat_id=? AND state=10;", - params![chat_id], - )?; + paramsv![chat_id], + ) + .await?; - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); @@ -1560,26 +1697,31 @@ pub fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> Ok(()) } -pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> { - if !context.sql.exists( - "SELECT id +pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> { + if !context + .sql + .exists( + "SELECT id FROM msgs WHERE state=10;", - params![], - )? { + paramsv![], + ) + .await? + { return Ok(()); } - sql::execute( - context, - &context.sql, - "UPDATE msgs + context + .sql + .execute( + "UPDATE msgs SET state=13 WHERE state=10;", - params![], - )?; + paramsv![], + ) + .await?; - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }); @@ -1591,14 +1733,16 @@ pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> { /// /// Returns true if any message is deleted, so event can be emitted. If nothing /// has been deleted, returns false. -pub fn delete_device_expired_messages(context: &Context) -> Result { - if let Some(delete_device_after) = context.get_config_delete_device_after() { +pub async fn delete_device_expired_messages(context: &Context) -> Result { + if let Some(delete_device_after) = context.get_config_delete_device_after().await { let threshold_timestamp = time() - delete_device_after; let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) + .await .unwrap_or_default() .0; let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) + .await .unwrap_or_default() .0; @@ -1606,21 +1750,24 @@ pub fn delete_device_expired_messages(context: &Context) -> Result // // Only update the rows that have to be updated, to avoid emitting // unnecessary "chat modified" events. - let rows_modified = context.sql.execute( - "UPDATE msgs \ + let rows_modified = context + .sql + .execute( + "UPDATE msgs \ SET txt = 'DELETED', chat_id = ? \ WHERE timestamp < ? \ AND chat_id > ? \ AND chat_id != ? \ AND chat_id != ?", - params![ - DC_CHAT_ID_TRASH, - threshold_timestamp, - DC_CHAT_ID_LAST_SPECIAL, - self_chat_id, - device_chat_id - ], - )?; + paramsv![ + DC_CHAT_ID_TRASH, + threshold_timestamp, + DC_CHAT_ID_LAST_SPECIAL, + self_chat_id, + device_chat_id + ], + ) + .await?; Ok(rows_modified > 0) } else { @@ -1628,7 +1775,7 @@ pub fn delete_device_expired_messages(context: &Context) -> Result } } -pub fn get_chat_media( +pub async fn get_chat_media( context: &Context, chat_id: ChatId, msg_type: Viewtype, @@ -1644,7 +1791,7 @@ pub fn get_chat_media( WHERE chat_id=? AND (type=? OR type=? OR type=?) ORDER BY timestamp, id;", - params![ + paramsv![ chat_id, msg_type, if msg_type2 != Viewtype::Unknown { @@ -1669,6 +1816,7 @@ pub fn get_chat_media( Ok(ret) }, ) + .await .unwrap_or_default() } @@ -1680,7 +1828,7 @@ pub enum Direction { Backward = -1, } -pub fn get_next_media( +pub async fn get_next_media( context: &Context, curr_msg_id: MsgId, direction: Direction, @@ -1690,7 +1838,7 @@ pub fn get_next_media( ) -> Option { let mut ret: Option = None; - if let Ok(msg) = Message::load_from_db(context, curr_msg_id) { + if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await { let list: Vec = get_chat_media( context, msg.chat_id, @@ -1701,7 +1849,8 @@ pub fn get_next_media( }, msg_type2, msg_type3, - ); + ) + .await; for (i, msg_id) in list.iter().enumerate() { if curr_msg_id == *msg_id { match direction { @@ -1723,7 +1872,7 @@ pub fn get_next_media( ret } -pub fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec { +pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec { /* Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a groupchat but the chats stays visible, moreover, this makes displaying lists easier) */ @@ -1743,49 +1892,53 @@ pub fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec { ON c.id=cc.contact_id WHERE cc.chat_id=? ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;", - params![chat_id], + paramsv![chat_id], |row| row.get::<_, u32>(0), |ids| ids.collect::, _>>().map_err(Into::into), ) + .await .unwrap_or_default() } -pub fn create_group_chat( +pub async fn create_group_chat( context: &Context, verified: VerifiedStatus, chat_name: impl AsRef, ) -> Result { ensure!(!chat_name.as_ref().is_empty(), "Invalid chat name"); - let draft_txt = context.stock_string_repl_str(StockMessage::NewGroupDraft, &chat_name); + let draft_txt = context + .stock_string_repl_str(StockMessage::NewGroupDraft, &chat_name) + .await; let grpid = dc_create_id(); - sql::execute( - context, - &context.sql, + context.sql.execute( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - params![ + paramsv![ if verified != VerifiedStatus::Unverified { Chattype::VerifiedGroup } else { Chattype::Group }, - chat_name.as_ref(), + chat_name.as_ref().to_string(), grpid, time(), ], - )?; + ).await?; - let row_id = sql::get_rowid(context, &context.sql, "chats", "grpid", grpid); + let row_id = context + .sql + .get_rowid(context, "chats", "grpid", grpid) + .await?; let chat_id = ChatId::new(row_id); if !chat_id.is_error() { - if add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF) { + if add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await { let mut draft_msg = Message::new(Viewtype::Text); draft_msg.set_text(Some(draft_txt)); - chat_id.set_draft_raw(context, &mut draft_msg); + chat_id.set_draft_raw(context, &mut draft_msg).await; } - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }); @@ -1795,18 +1948,20 @@ pub fn create_group_chat( } /// add a contact to the chats_contact table -pub(crate) fn add_to_chat_contacts_table( +pub(crate) async fn add_to_chat_contacts_table( context: &Context, chat_id: ChatId, contact_id: u32, ) -> bool { - match sql::execute( - context, - &context.sql, - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", - params![chat_id, contact_id as i32], - ) { - Ok(()) => true, + match context + .sql + .execute( + "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", + paramsv![chat_id, contact_id as i32], + ) + .await + { + Ok(_) => true, Err(err) => { error!( context, @@ -1819,18 +1974,20 @@ pub(crate) fn add_to_chat_contacts_table( } /// remove a contact from the chats_contact table -pub(crate) fn remove_from_chat_contacts_table( +pub(crate) async fn remove_from_chat_contacts_table( context: &Context, chat_id: ChatId, contact_id: u32, ) -> bool { - match sql::execute( - context, - &context.sql, - "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", - params![chat_id, contact_id as i32], - ) { - Ok(()) => true, + match context + .sql + .execute( + "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", + paramsv![chat_id, contact_id as i32], + ) + .await + { + Ok(_) => true, Err(_) => { warn!( context, @@ -1843,8 +2000,8 @@ pub(crate) fn remove_from_chat_contacts_table( } /// Adds a contact to the chat. -pub fn add_contact_to_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool { - match add_contact_to_chat_ex(context, chat_id, contact_id, false) { +pub async fn add_contact_to_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool { + match add_contact_to_chat_ex(context, chat_id, contact_id, false).await { Ok(res) => res, Err(err) => { error!(context, "failed to add contact: {}", err); @@ -1853,32 +2010,32 @@ pub fn add_contact_to_chat(context: &Context, chat_id: ChatId, contact_id: u32) } } -pub(crate) fn add_contact_to_chat_ex( +pub(crate) async fn add_contact_to_chat_ex( context: &Context, chat_id: ChatId, contact_id: u32, from_handshake: bool, ) -> Result { ensure!(!chat_id.is_special(), "can not add member to special chats"); - let contact = Contact::get_by_id(context, contact_id)?; + let contact = Contact::get_by_id(context, contact_id).await?; let mut msg = Message::default(); - reset_gossiped_timestamp(context, chat_id)?; + reset_gossiped_timestamp(context, chat_id).await?; /*this also makes sure, not contacts are added to special or normal chats*/ - let mut chat = Chat::load_from_db(context, chat_id)?; + let mut chat = Chat::load_from_db(context, chat_id).await?; ensure!( - real_group_exists(context, chat_id), + real_group_exists(context, chat_id).await, "{} is not a group where one can add members", chat_id ); ensure!( - Contact::real_exists_by_id(context, contact_id) || contact_id == DC_CONTACT_ID_SELF, + Contact::real_exists_by_id(context, contact_id).await || contact_id == DC_CONTACT_ID_SELF, "invalid contact_id {} for adding to group", contact_id ); - if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF as u32) { + if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF as u32).await { /* we should respect this - whatever we send to the group, it gets discarded anyway! */ emit_event!( context, @@ -1888,10 +2045,11 @@ pub(crate) fn add_contact_to_chat_ex( } if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { chat.param.remove(Param::Unpromoted); - chat.update_param(context)?; + chat.update_param(context).await?; } let self_addr = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); if addr_cmp(contact.get_addr(), &self_addr) { // ourself is added using DC_CONTACT_ID_SELF, do not add this address explicitly. @@ -1903,14 +2061,14 @@ pub(crate) fn add_contact_to_chat_ex( return Ok(false); } - if is_contact_in_chat(context, chat_id, contact_id) { + if is_contact_in_chat(context, chat_id, contact_id).await { if !from_handshake { return Ok(true); } } else { // else continue and send status mail if chat.typ == Chattype::VerifiedGroup - && contact.is_verified(context) != VerifiedStatus::BidirectVerified + && contact.is_verified(context).await != VerifiedStatus::BidirectVerified { error!( context, @@ -1918,31 +2076,34 @@ pub(crate) fn add_contact_to_chat_ex( ); return Ok(false); } - if !add_to_chat_contacts_table(context, chat_id, contact_id) { + if !add_to_chat_contacts_table(context, chat_id, contact_id).await { return Ok(false); } } if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 { msg.viewtype = Viewtype::Text; - msg.text = Some(context.stock_system_msg( - StockMessage::MsgAddMember, - contact.get_addr(), - "", - DC_CONTACT_ID_SELF as u32, - )); + msg.text = Some( + context + .stock_system_msg( + StockMessage::MsgAddMember, + contact.get_addr(), + "", + DC_CONTACT_ID_SELF as u32, + ) + .await, + ); msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact.get_addr()); msg.param.set_int(Param::Arg2, from_handshake.into()); - - msg.id = send_msg(context, chat_id, &mut msg)?; + msg.id = send_msg(context, chat_id, &mut msg).await?; } - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); Ok(true) } -fn real_group_exists(context: &Context, chat_id: ChatId) -> bool { +async fn real_group_exists(context: &Context, chat_id: ChatId) -> bool { // check if a group or a verified group exists under the given ID - if !context.sql.is_open() || chat_id.is_special() { + if !context.sql.is_open().await || chat_id.is_special() { return false; } @@ -1950,29 +2111,34 @@ fn real_group_exists(context: &Context, chat_id: ChatId) -> bool { .sql .exists( "SELECT id FROM chats WHERE id=? AND (type=120 OR type=130);", - params![chat_id], + paramsv![chat_id], ) + .await .unwrap_or_default() } -pub(crate) fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<(), Error> { - set_gossiped_timestamp(context, chat_id, 0) +pub(crate) async fn reset_gossiped_timestamp( + context: &Context, + chat_id: ChatId, +) -> Result<(), Error> { + set_gossiped_timestamp(context, chat_id, 0).await } /// Get timestamp of the last gossip sent in the chat. /// Zero return value means that gossip was never sent. -pub fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> i64 { +pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> i64 { context .sql - .query_get_value::<_, i64>( + .query_get_value::( context, "SELECT gossiped_timestamp FROM chats WHERE id=?;", - params![chat_id], + paramsv![chat_id], ) + .await .unwrap_or_default() } -pub(crate) fn set_gossiped_timestamp( +pub(crate) async fn set_gossiped_timestamp( context: &Context, chat_id: ChatId, timestamp: i64, @@ -1983,47 +2149,56 @@ pub(crate) fn set_gossiped_timestamp( "set gossiped_timestamp for chat #{} to {}.", chat_id, timestamp, ); - sql::execute( - context, - &context.sql, - "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", - params![timestamp, chat_id], - )?; + context + .sql + .execute( + "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", + paramsv![timestamp, chat_id], + ) + .await?; + Ok(()) } -pub(crate) fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result { +pub(crate) async fn shall_attach_selfavatar( + context: &Context, + chat_id: ChatId, +) -> Result { // versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others. // to avoid sending out previously set selfavatars unexpectedly we added this additional check. // it can be removed after some time. if !context .sql .get_raw_config_bool(context, "attach_selfavatar") + .await { return Ok(false); } let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60; - let needs_attach = context.sql.query_map( - "SELECT c.selfavatar_sent + let needs_attach = context + .sql + .query_map( + "SELECT c.selfavatar_sent FROM chats_contacts cc LEFT JOIN contacts c ON c.id=cc.contact_id WHERE cc.chat_id=? AND cc.contact_id!=?;", - params![chat_id, DC_CONTACT_ID_SELF], - |row| Ok(row.get::<_, i64>(0)), - |rows| { - let mut needs_attach = false; - for row in rows { - if let Ok(selfavatar_sent) = row { - let selfavatar_sent = selfavatar_sent?; - if selfavatar_sent < timestamp_some_days_ago { - needs_attach = true; + paramsv![chat_id, DC_CONTACT_ID_SELF], + |row| Ok(row.get::<_, i64>(0)), + |rows| { + let mut needs_attach = false; + for row in rows { + if let Ok(selfavatar_sent) = row { + let selfavatar_sent = selfavatar_sent?; + if selfavatar_sent < timestamp_some_days_ago { + needs_attach = true; + } } } - } - Ok(needs_attach) - }, - )?; + Ok(needs_attach) + }, + ) + .await?; Ok(needs_attach) } @@ -2069,24 +2244,29 @@ impl rusqlite::types::FromSql for MuteDuration { } } -pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> { +pub async fn set_muted( + context: &Context, + chat_id: ChatId, + duration: MuteDuration, +) -> Result<(), Error> { ensure!(!chat_id.is_special(), "Invalid chat ID"); - if sql::execute( - context, - &context.sql, - "UPDATE chats SET muted_until=? WHERE id=?;", - params![duration, chat_id], - ) - .is_ok() + if context + .sql + .execute( + "UPDATE chats SET muted_until=? WHERE id=?;", + paramsv![duration, chat_id], + ) + .await + .is_ok() { - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); } else { bail!("Failed to set mute duration, chat might not exist -"); } Ok(()) } -pub fn remove_contact_from_chat( +pub async fn remove_contact_from_chat( context: &Context, chat_id: ChatId, contact_id: u32, @@ -2106,9 +2286,9 @@ pub fn remove_contact_from_chat( /* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */ /* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */ - if let Ok(chat) = Chat::load_from_db(context, chat_id) { - if real_group_exists(context, chat_id) { - if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) { + if let Ok(chat) = Chat::load_from_db(context, chat_id).await { + if real_group_exists(context, chat_id).await { + if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await { emit_event!( context, Event::ErrorSelfNotInGroup( @@ -2116,28 +2296,36 @@ pub fn remove_contact_from_chat( ) ); } else { - if let Ok(contact) = Contact::get_by_id(context, contact_id) { + if let Ok(contact) = Contact::get_by_id(context, contact_id).await { if chat.is_promoted() { msg.viewtype = Viewtype::Text; if contact.id == DC_CONTACT_ID_SELF { - set_group_explicitly_left(context, chat.grpid)?; - msg.text = Some(context.stock_system_msg( - StockMessage::MsgGroupLeft, - "", - "", - DC_CONTACT_ID_SELF, - )); + set_group_explicitly_left(context, chat.grpid).await?; + msg.text = Some( + context + .stock_system_msg( + StockMessage::MsgGroupLeft, + "", + "", + DC_CONTACT_ID_SELF, + ) + .await, + ); } else { - msg.text = Some(context.stock_system_msg( - StockMessage::MsgDelMember, - contact.get_addr(), - "", - DC_CONTACT_ID_SELF, - )); + msg.text = Some( + context + .stock_system_msg( + StockMessage::MsgDelMember, + contact.get_addr(), + "", + DC_CONTACT_ID_SELF, + ) + .await, + ); } msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); msg.param.set(Param::Arg, contact.get_addr()); - msg.id = send_msg(context, chat_id, &mut msg)?; + msg.id = send_msg(context, chat_id, &mut msg).await?; } } // we remove the member from the chat after constructing the @@ -2150,8 +2338,8 @@ pub fn remove_contact_from_chat( // in order to correctly determine encryption so if we // removed it first, it would complicate the // check/encryption logic. - success = remove_from_chat_contacts_table(context, chat_id, contact_id); - context.call_cb(Event::ChatModified(chat_id)); + success = remove_from_chat_contacts_table(context, chat_id, contact_id).await; + context.emit_event(Event::ChatModified(chat_id)); } } } @@ -2163,20 +2351,21 @@ pub fn remove_contact_from_chat( Ok(()) } -fn set_group_explicitly_left(context: &Context, grpid: impl AsRef) -> Result<(), Error> { - if !is_group_explicitly_left(context, grpid.as_ref())? { - sql::execute( - context, - &context.sql, - "INSERT INTO leftgrps (grpid) VALUES(?);", - params![grpid.as_ref()], - )?; +async fn set_group_explicitly_left(context: &Context, grpid: impl AsRef) -> Result<(), Error> { + if !is_group_explicitly_left(context, grpid.as_ref()).await? { + context + .sql + .execute( + "INSERT INTO leftgrps (grpid) VALUES(?);", + paramsv![grpid.as_ref().to_string()], + ) + .await?; } Ok(()) } -pub(crate) fn is_group_explicitly_left( +pub(crate) async fn is_group_explicitly_left( context: &Context, grpid: impl AsRef, ) -> Result { @@ -2184,12 +2373,13 @@ pub(crate) fn is_group_explicitly_left( .sql .exists( "SELECT id FROM leftgrps WHERE grpid=?;", - params![grpid.as_ref()], + paramsv![grpid.as_ref()], ) + .await .map_err(Into::into) } -pub fn set_chat_name( +pub async fn set_chat_name( context: &Context, chat_id: ChatId, new_name: impl AsRef, @@ -2200,46 +2390,51 @@ pub fn set_chat_name( ensure!(!new_name.as_ref().is_empty(), "Invalid name"); ensure!(!chat_id.is_special(), "Invalid chat ID"); - let chat = Chat::load_from_db(context, chat_id)?; + let chat = Chat::load_from_db(context, chat_id).await?; let mut msg = Message::default(); - if real_group_exists(context, chat_id) { + if real_group_exists(context, chat_id).await { if chat.name == new_name.as_ref() { success = true; - } else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) { + } else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await { emit_event!( context, Event::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into()) ); } else { /* we should respect this - whatever we send to the group, it gets discarded anyway! */ - if sql::execute( - context, - &context.sql, - "UPDATE chats SET name=? WHERE id=?;", - params![new_name.as_ref(), chat_id], - ) - .is_ok() + if context + .sql + .execute( + "UPDATE chats SET name=? WHERE id=?;", + paramsv![new_name.as_ref().to_string(), chat_id], + ) + .await + .is_ok() { if chat.is_promoted() { msg.viewtype = Viewtype::Text; - msg.text = Some(context.stock_system_msg( - StockMessage::MsgGrpName, - &chat.name, - new_name.as_ref(), - DC_CONTACT_ID_SELF, - )); + msg.text = Some( + context + .stock_system_msg( + StockMessage::MsgGrpName, + &chat.name, + new_name.as_ref(), + DC_CONTACT_ID_SELF, + ) + .await, + ); msg.param.set_cmd(SystemMessage::GroupNameChanged); if !chat.name.is_empty() { msg.param.set(Param::Arg, &chat.name); } - msg.id = send_msg(context, chat_id, &mut msg)?; - context.call_cb(Event::MsgsChanged { + msg.id = send_msg(context, chat_id, &mut msg).await?; + context.emit_event(Event::MsgsChanged { chat_id, msg_id: msg.id, }); } - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); success = true; } } @@ -2257,19 +2452,19 @@ pub fn set_chat_name( /// The profile image can only be set when you are a member of the /// chat. To remove the profile image pass an empty string for the /// `new_image` parameter. -pub fn set_chat_profile_image( +pub async fn set_chat_profile_image( context: &Context, chat_id: ChatId, new_image: impl AsRef, // XXX use PathBuf ) -> Result<(), Error> { ensure!(!chat_id.is_special(), "Invalid chat ID"); - let mut chat = Chat::load_from_db(context, chat_id)?; + let mut chat = Chat::load_from_db(context, chat_id).await?; ensure!( - real_group_exists(context, chat_id), + real_group_exists(context, chat_id).await, "Failed to set profile image; group does not exist" ); /* we should respect this - whatever we send to the group, it gets discarded anyway! */ - if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) { + if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await { emit_event!( context, Event::ErrorSelfNotInGroup("Cannot set chat profile image; self not in group.".into()) @@ -2282,34 +2477,33 @@ pub fn set_chat_profile_image( if new_image.as_ref().is_empty() { chat.param.remove(Param::ProfileImage); msg.param.remove(Param::Arg); - msg.text = Some(context.stock_system_msg( - StockMessage::MsgGrpImgDeleted, - "", - "", - DC_CONTACT_ID_SELF, - )); + msg.text = Some( + context + .stock_system_msg(StockMessage::MsgGrpImgDeleted, "", "", DC_CONTACT_ID_SELF) + .await, + ); } else { - let image_blob = BlobObject::from_path(context, Path::new(new_image.as_ref())).or_else( - |err| match err { + let image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) { + Ok(blob) => Ok(blob), + Err(err) => match err { BlobError::WrongBlobdir { .. } => { - BlobObject::create_and_copy(context, Path::new(new_image.as_ref())) + BlobObject::create_and_copy(context, Path::new(new_image.as_ref())).await } _ => Err(err), }, - )?; + }?; image_blob.recode_to_avatar_size(context)?; chat.param.set(Param::ProfileImage, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name()); - msg.text = Some(context.stock_system_msg( - StockMessage::MsgGrpImgChanged, - "", - "", - DC_CONTACT_ID_SELF, - )); + msg.text = Some( + context + .stock_system_msg(StockMessage::MsgGrpImgChanged, "", "", DC_CONTACT_ID_SELF) + .await, + ); } - chat.update_param(context)?; + chat.update_param(context).await?; if chat.is_promoted() { - msg.id = send_msg(context, chat_id, &mut msg)?; + msg.id = send_msg(context, chat_id, &mut msg).await?; emit_event!( context, Event::MsgsChanged { @@ -2322,7 +2516,11 @@ pub fn set_chat_profile_image( Ok(()) } -pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<(), Error> { +pub async fn forward_msgs( + context: &Context, + msg_ids: &[MsgId], + chat_id: ChatId, +) -> Result<(), Error> { ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward"); ensure!(!chat_id.is_special(), "can not forward to special chat"); @@ -2330,23 +2528,26 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re let mut created_msgs: Vec = Vec::new(); let mut curr_timestamp: i64; - chat_id.unarchive(context)?; - if let Ok(mut chat) = Chat::load_from_db(context, chat_id) { + chat_id.unarchive(context).await?; + if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { ensure!(chat.can_send(), "cannot send to {}", chat_id); - curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()); - let ids = context.sql.query_map( - format!( - "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", - msg_ids.iter().map(|_| "?").join(",") - ), - msg_ids, - |row| row.get::<_, MsgId>(0), - |ids| ids.collect::, _>>().map_err(Into::into), - )?; + curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()).await; + let ids = context + .sql + .query_map( + format!( + "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", + msg_ids.iter().map(|_| "?").join(",") + ), + msg_ids.iter().map(|v| v as &dyn crate::ToSql).collect(), + |row| row.get::<_, MsgId>(0), + |ids| ids.collect::, _>>().map_err(Into::into), + ) + .await?; for id in ids { let src_msg_id: MsgId = id; - let msg = Message::load_from_db(context, src_msg_id); + let msg = Message::load_from_db(context, src_msg_id).await; if msg.is_err() { break; } @@ -2366,7 +2567,7 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re if msg.state == MessageState::OutPreparing { let fresh9 = curr_timestamp; curr_timestamp += 1; - new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh9)?; + new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh9).await?; let save_param = msg.param.clone(); msg.param = original_param; msg.id = src_msg_id; @@ -2379,21 +2580,23 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re .set(Param::PrepForwards, new_msg_id.to_u32().to_string()); } - msg.save_param_to_disk(context); + msg.save_param_to_disk(context).await; msg.param = save_param; } else { msg.state = MessageState::OutPending; let fresh10 = curr_timestamp; curr_timestamp += 1; - new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh10)?; - job_send_msg(context, new_msg_id)?; + new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh10).await?; + if let Some(send_job) = job::send_msg_job(context, new_msg_id).await? { + job::add(context, send_job).await; + } } created_chats.push(chat_id); created_msgs.push(new_msg_id); } } for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: *chat_id, msg_id: *msg_id, }); @@ -2401,54 +2604,59 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re Ok(()) } -pub(crate) fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize { +pub(crate) async fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize { context .sql - .query_get_value::<_, isize>( + .query_get_value::( context, "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;", - params![chat_id], + paramsv![chat_id], ) + .await .unwrap_or_default() as usize } -pub(crate) fn get_chat_cnt(context: &Context) -> usize { - if context.sql.is_open() { +pub(crate) async fn get_chat_cnt(context: &Context) -> usize { + if context.sql.is_open().await { /* no database, no chats - this is no error (needed eg. for information) */ context .sql - .query_get_value::<_, isize>( + .query_get_value::( context, "SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;", - params![], + paramsv![], ) + .await .unwrap_or_default() as usize } else { 0 } } -pub(crate) fn get_chat_id_by_grpid( +pub(crate) async fn get_chat_id_by_grpid( context: &Context, grpid: impl AsRef, ) -> Result<(ChatId, bool, Blocked), sql::Error> { - context.sql.query_row( - "SELECT id, blocked, type FROM chats WHERE grpid=?;", - params![grpid.as_ref()], - |row| { - let chat_id = row.get::<_, ChatId>(0)?; + context + .sql + .query_row( + "SELECT id, blocked, type FROM chats WHERE grpid=?;", + paramsv![grpid.as_ref()], + |row| { + let chat_id = row.get::<_, ChatId>(0)?; - let b = row.get::<_, Option>(1)?.unwrap_or_default(); - let v = row.get::<_, Option>(2)?.unwrap_or_default(); - Ok((chat_id, v == Chattype::VerifiedGroup, b)) - }, - ) + let b = row.get::<_, Option>(1)?.unwrap_or_default(); + let v = row.get::<_, Option>(2)?.unwrap_or_default(); + Ok((chat_id, v == Chattype::VerifiedGroup, b)) + }, + ) + .await } /// Adds a message to device chat. /// /// Optional `label` can be provided to ensure that message is added only once. -pub fn add_device_msg( +pub async fn add_device_msg( context: &Context, label: Option<&str>, msg: Option<&mut Message>, @@ -2461,61 +2669,73 @@ pub fn add_device_msg( let mut msg_id = MsgId::new_unset(); if let Some(label) = label { - if was_device_msg_ever_added(context, label)? { + if was_device_msg_ever_added(context, label).await? { info!(context, "device-message {} already added", label); return Ok(msg_id); } } if let Some(msg) = msg { - chat_id = create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?.0; + chat_id = create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not) + .await? + .0; let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); - msg.try_calc_and_set_dimensions(context).ok(); - prepare_msg_blob(context, msg)?; - chat_id.unarchive(context)?; + msg.try_calc_and_set_dimensions(context).await.ok(); + prepare_msg_blob(context, msg).await?; + chat_id.unarchive(context).await?; context.sql.execute( "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \ VALUES (?,?,?, ?,?,?, ?,?,?);", - params![ + paramsv![ chat_id, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, - dc_create_smeared_timestamp(context), + dc_create_smeared_timestamp(context).await, msg.viewtype, MessageState::InFresh, - msg.text.as_ref().map_or("", String::as_str), + msg.text.as_ref().cloned().unwrap_or_default(), msg.param.to_string(), rfc724_mid, ], - )?; + ).await?; - let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); + let row_id = context + .sql + .get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid) + .await?; msg_id = MsgId::new(row_id); } if let Some(label) = label { - context.sql.execute( - "INSERT INTO devmsglabels (label) VALUES (?);", - params![label], - )?; + context + .sql + .execute( + "INSERT INTO devmsglabels (label) VALUES (?);", + paramsv![label.to_string()], + ) + .await?; } if !msg_id.is_unset() { - context.call_cb(Event::IncomingMsg { chat_id, msg_id }); + context.emit_event(Event::IncomingMsg { chat_id, msg_id }); } Ok(msg_id) } -pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result { +pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result { ensure!(!label.is_empty(), "empty label"); - if let Ok(()) = context.sql.query_row( - "SELECT label FROM devmsglabels WHERE label=?", - params![label], - |_| Ok(()), - ) { + if let Ok(()) = context + .sql + .query_row( + "SELECT label FROM devmsglabels WHERE label=?", + paramsv![label], + |_| Ok(()), + ) + .await + { return Ok(true); } @@ -2527,41 +2747,49 @@ pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result Result<(), Error> { - context.sql.execute( - "DELETE FROM msgs WHERE from_id=?;", - params![DC_CONTACT_ID_DEVICE], - )?; +pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> { context .sql - .execute("DELETE FROM devmsglabels;", params![])?; + .execute( + "DELETE FROM msgs WHERE from_id=?;", + paramsv![DC_CONTACT_ID_DEVICE], + ) + .await?; + context + .sql + .execute("DELETE FROM devmsglabels;", paramsv![]) + .await?; Ok(()) } /// Adds an informational message to chat. /// /// For example, it can be a message showing that a member was added to a group. -pub(crate) fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef) { +pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef) { let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); if context.sql.execute( "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);", - params![ + paramsv![ chat_id, DC_CONTACT_ID_INFO, DC_CONTACT_ID_INFO, - dc_create_smeared_timestamp(context), + dc_create_smeared_timestamp(context).await, Viewtype::Text, MessageState::InNoticed, - text.as_ref(), + text.as_ref().to_string(), rfc724_mid, ] - ).is_err() { + ).await.is_err() { return; } - let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); - context.call_cb(Event::MsgsChanged { + let row_id = context + .sql + .get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid) + .await + .unwrap_or_default(); + context.emit_event(Event::MsgsChanged { chat_id, msg_id: MsgId::new(row_id), }); @@ -2574,13 +2802,15 @@ mod tests { use crate::contact::Contact; use crate::test_utils::*; - #[test] - fn test_chat_info() { - let t = dummy_context(); - let bob = Contact::create(&t.ctx, "bob", "bob@example.com").unwrap(); - let chat_id = create_by_contact_id(&t.ctx, bob).unwrap(); - let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap(); - let info = chat.get_info(&t.ctx).unwrap(); + #[async_std::test] + async fn test_chat_info() { + let t = dummy_context().await; + let bob = Contact::create(&t.ctx, "bob", "bob@example.com") + .await + .unwrap(); + let chat_id = create_by_contact_id(&t.ctx, bob).await.unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + let info = chat.get_info(&t.ctx).await.unwrap(); // Ensure we can serialize this. println!("{}", serde_json::to_string_pretty(&info).unwrap()); @@ -2606,101 +2836,117 @@ mod tests { assert_eq!(info, loaded); } - #[test] - fn test_get_draft_no_draft() { - let t = dummy_context(); - let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); - let draft = chat_id.get_draft(&t.ctx).unwrap(); + #[async_std::test] + async fn test_get_draft_no_draft() { + let t = dummy_context().await; + let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); + let draft = chat_id.get_draft(&t.ctx).await.unwrap(); assert!(draft.is_none()); } - #[test] - fn test_get_draft_special_chat_id() { - let t = dummy_context(); + #[async_std::test] + async fn test_get_draft_special_chat_id() { + let t = dummy_context().await; let draft = ChatId::new(DC_CHAT_ID_LAST_SPECIAL) .get_draft(&t.ctx) + .await .unwrap(); assert!(draft.is_none()); } - #[test] - fn test_get_draft_no_chat() { + #[async_std::test] + async fn test_get_draft_no_chat() { // This is a weird case, maybe this should be an error but we // do not get this info from the database currently. - let t = dummy_context(); - let draft = ChatId::new(42).get_draft(&t.ctx).unwrap(); + let t = dummy_context().await; + let draft = ChatId::new(42).get_draft(&t.ctx).await.unwrap(); assert!(draft.is_none()); } - #[test] - fn test_get_draft() { - let t = dummy_context(); - let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); + #[async_std::test] + async fn test_get_draft() { + let t = dummy_context().await; + let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("hello".to_string())); - chat_id.set_draft(&t.ctx, Some(&mut msg)); - let draft = chat_id.get_draft(&t.ctx).unwrap().unwrap(); + chat_id.set_draft(&t.ctx, Some(&mut msg)).await; + let draft = chat_id.get_draft(&t.ctx).await.unwrap().unwrap(); let msg_text = msg.get_text(); let draft_text = draft.get_text(); assert_eq!(msg_text, draft_text); } - #[test] - fn test_add_contact_to_chat_ex_add_self() { + #[async_std::test] + async fn test_add_contact_to_chat_ex_add_self() { // Adding self to a contact should succeed, even though it's pointless. - let t = test_context(Some(Box::new(logging_cb))); - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); - let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false).unwrap(); + let t = test_context().await; + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); + let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false) + .await + .unwrap(); assert_eq!(added, false); } - #[test] - fn test_self_talk() { - let t = dummy_context(); - let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); + #[async_std::test] + async fn test_self_talk() { + let t = dummy_context().await; + let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); assert_eq!(DC_CONTACT_ID_SELF, 1); assert!(!chat_id.is_special()); - let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); assert_eq!(chat.id, chat_id); assert!(chat.is_self_talk()); assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(chat.can_send()); - assert_eq!(chat.name, t.ctx.stock_str(StockMessage::SavedMessages)); - assert!(chat.get_profile_image(&t.ctx).is_some()); + assert_eq!( + chat.name, + t.ctx.stock_str(StockMessage::SavedMessages).await + ); + assert!(chat.get_profile_image(&t.ctx).await.is_some()); } - #[test] - fn test_deaddrop_chat() { - let t = dummy_context(); - let chat = Chat::load_from_db(&t.ctx, ChatId::new(DC_CHAT_ID_DEADDROP)).unwrap(); + #[async_std::test] + async fn test_deaddrop_chat() { + let t = dummy_context().await; + let chat = Chat::load_from_db(&t.ctx, ChatId::new(DC_CHAT_ID_DEADDROP)) + .await + .unwrap(); assert_eq!(DC_CHAT_ID_DEADDROP, 1); assert!(chat.id.is_deaddrop()); assert!(!chat.is_self_talk()); assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeadDrop)); + assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeadDrop).await); } - #[test] - fn test_add_device_msg_unlabelled() { - let t = test_context(Some(Box::new(logging_cb))); + #[async_std::test] + async fn test_add_device_msg_unlabelled() { + let t = test_context().await; // add two device-messages let mut msg1 = Message::new(Viewtype::Text); msg1.text = Some("first message".to_string()); - let msg1_id = add_device_msg(&t.ctx, None, Some(&mut msg1)); + let msg1_id = add_device_msg(&t.ctx, None, Some(&mut msg1)).await; assert!(msg1_id.is_ok()); let mut msg2 = Message::new(Viewtype::Text); msg2.text = Some("second message".to_string()); - let msg2_id = add_device_msg(&t.ctx, None, Some(&mut msg2)); + let msg2_id = add_device_msg(&t.ctx, None, Some(&mut msg2)).await; assert!(msg2_id.is_ok()); assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap()); // check added messages - let msg1 = message::Message::load_from_db(&t.ctx, msg1_id.unwrap()); + let msg1 = message::Message::load_from_db(&t.ctx, msg1_id.unwrap()).await; assert!(msg1.is_ok()); let msg1 = msg1.unwrap(); assert_eq!(msg1.text.as_ref().unwrap(), "first message"); @@ -2709,34 +2955,34 @@ mod tests { assert!(!msg1.is_info()); assert!(!msg1.is_setupmessage()); - let msg2 = message::Message::load_from_db(&t.ctx, msg2_id.unwrap()); + let msg2 = message::Message::load_from_db(&t.ctx, msg2_id.unwrap()).await; assert!(msg2.is_ok()); let msg2 = msg2.unwrap(); assert_eq!(msg2.text.as_ref().unwrap(), "second message"); // check device chat - assert_eq!(msg2.chat_id.get_msg_cnt(&t.ctx), 2); + assert_eq!(msg2.chat_id.get_msg_cnt(&t.ctx).await, 2); } - #[test] - fn test_add_device_msg_labelled() { - let t = test_context(Some(Box::new(logging_cb))); + #[async_std::test] + async fn test_add_device_msg_labelled() { + let t = test_context().await; // add two device-messages with the same label (second attempt is not added) let mut msg1 = Message::new(Viewtype::Text); msg1.text = Some("first message".to_string()); - let msg1_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg1)); + let msg1_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg1)).await; assert!(msg1_id.is_ok()); assert!(!msg1_id.as_ref().unwrap().is_unset()); let mut msg2 = Message::new(Viewtype::Text); msg2.text = Some("second message".to_string()); - let msg2_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)); + let msg2_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)).await; assert!(msg2_id.is_ok()); assert!(msg2_id.as_ref().unwrap().is_unset()); // check added message - let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()); + let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()).await; assert!(msg1.is_ok()); let msg1 = msg1.unwrap(); assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id); @@ -2748,212 +2994,258 @@ mod tests { // check device chat let chat_id = msg1.chat_id; - assert_eq!(chat_id.get_msg_cnt(&t.ctx), 1); + assert_eq!(chat_id.get_msg_cnt(&t.ctx).await, 1); assert!(!chat_id.is_special()); - let chat = Chat::load_from_db(&t.ctx, chat_id); + let chat = Chat::load_from_db(&t.ctx, chat_id).await; assert!(chat.is_ok()); let chat = chat.unwrap(); assert_eq!(chat.get_type(), Chattype::Single); assert!(chat.is_device_talk()); assert!(!chat.is_self_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeviceMessages)); - assert!(chat.get_profile_image(&t.ctx).is_some()); + assert_eq!( + chat.name, + t.ctx.stock_str(StockMessage::DeviceMessages).await + ); + assert!(chat.get_profile_image(&t.ctx).await.is_some()); // delete device message, make sure it is not added again - message::delete_msgs(&t.ctx, &[*msg1_id.as_ref().unwrap()]); - let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()); + message::delete_msgs(&t.ctx, &[*msg1_id.as_ref().unwrap()]).await; + let msg1 = message::Message::load_from_db(&t.ctx, *msg1_id.as_ref().unwrap()).await; assert!(msg1.is_err() || msg1.unwrap().chat_id.is_trash()); - let msg3_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)); + let msg3_id = add_device_msg(&t.ctx, Some("any-label"), Some(&mut msg2)).await; assert!(msg3_id.is_ok()); assert!(msg2_id.as_ref().unwrap().is_unset()); } - #[test] - fn test_add_device_msg_label_only() { - let t = test_context(Some(Box::new(logging_cb))); - let res = add_device_msg(&t.ctx, Some(""), None); + #[async_std::test] + async fn test_add_device_msg_label_only() { + let t = test_context().await; + let res = add_device_msg(&t.ctx, Some(""), None).await; assert!(res.is_err()); - let res = add_device_msg(&t.ctx, Some("some-label"), None); + let res = add_device_msg(&t.ctx, Some("some-label"), None).await; assert!(res.is_ok()); let mut msg = Message::new(Viewtype::Text); msg.text = Some("message text".to_string()); - let msg_id = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)); + let msg_id = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).await; assert!(msg_id.is_ok()); assert!(msg_id.as_ref().unwrap().is_unset()); - let msg_id = add_device_msg(&t.ctx, Some("unused-label"), Some(&mut msg)); + let msg_id = add_device_msg(&t.ctx, Some("unused-label"), Some(&mut msg)).await; assert!(msg_id.is_ok()); assert!(!msg_id.as_ref().unwrap().is_unset()); } - #[test] - fn test_was_device_msg_ever_added() { - let t = test_context(Some(Box::new(logging_cb))); - add_device_msg(&t.ctx, Some("some-label"), None).ok(); - assert!(was_device_msg_ever_added(&t.ctx, "some-label").unwrap()); + #[async_std::test] + async fn test_was_device_msg_ever_added() { + let t = test_context().await; + add_device_msg(&t.ctx, Some("some-label"), None).await.ok(); + assert!(was_device_msg_ever_added(&t.ctx, "some-label") + .await + .unwrap()); let mut msg = Message::new(Viewtype::Text); msg.text = Some("message text".to_string()); - add_device_msg(&t.ctx, Some("another-label"), Some(&mut msg)).ok(); - assert!(was_device_msg_ever_added(&t.ctx, "another-label").unwrap()); + add_device_msg(&t.ctx, Some("another-label"), Some(&mut msg)) + .await + .ok(); + assert!(was_device_msg_ever_added(&t.ctx, "another-label") + .await + .unwrap()); - assert!(!was_device_msg_ever_added(&t.ctx, "unused-label").unwrap()); + assert!(!was_device_msg_ever_added(&t.ctx, "unused-label") + .await + .unwrap()); - assert!(was_device_msg_ever_added(&t.ctx, "").is_err()); + assert!(was_device_msg_ever_added(&t.ctx, "").await.is_err()); } - #[test] - fn test_delete_device_chat() { - let t = test_context(Some(Box::new(logging_cb))); + #[async_std::test] + async fn test_delete_device_chat() { + let t = test_context().await; let mut msg = Message::new(Viewtype::Text); msg.text = Some("message text".to_string()); - add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).ok(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)) + .await + .ok(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); // after the device-chat and all messages are deleted, a re-adding should do nothing - chats.get_chat_id(0).delete(&t.ctx).ok(); - add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).ok(); - assert_eq!(chatlist_len(&t.ctx, 0), 0) + chats.get_chat_id(0).delete(&t.ctx).await.ok(); + add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)) + .await + .ok(); + assert_eq!(chatlist_len(&t.ctx, 0).await, 0) } - #[test] - fn test_device_chat_cannot_sent() { - let t = test_context(Some(Box::new(logging_cb))); - t.ctx.update_device_chats().unwrap(); + #[async_std::test] + async fn test_device_chat_cannot_sent() { + let t = test_context().await; + t.ctx.update_device_chats().await.unwrap(); let (device_chat_id, _) = - create_or_lookup_by_contact_id(&t.ctx, DC_CONTACT_ID_DEVICE, Blocked::Not).unwrap(); + create_or_lookup_by_contact_id(&t.ctx, DC_CONTACT_ID_DEVICE, Blocked::Not) + .await + .unwrap(); let mut msg = Message::new(Viewtype::Text); msg.text = Some("message text".to_string()); - assert!(send_msg(&t.ctx, device_chat_id, &mut msg).is_err()); - assert!(prepare_msg(&t.ctx, device_chat_id, &mut msg).is_err()); + assert!(send_msg(&t.ctx, device_chat_id, &mut msg).await.is_err()); + assert!(prepare_msg(&t.ctx, device_chat_id, &mut msg).await.is_err()); - let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); - assert!(forward_msgs(&t.ctx, &[msg_id], device_chat_id).is_err()); + let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).await.unwrap(); + assert!(forward_msgs(&t.ctx, &[msg_id], device_chat_id) + .await + .is_err()); } - #[test] - fn test_delete_and_reset_all_device_msgs() { - let t = test_context(Some(Box::new(logging_cb))); + #[async_std::test] + async fn test_delete_and_reset_all_device_msgs() { + let t = test_context().await; let mut msg = Message::new(Viewtype::Text); msg.text = Some("message text".to_string()); - let msg_id1 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).unwrap(); + let msg_id1 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); // adding a device message with the same label won't be executed again ... - assert!(was_device_msg_ever_added(&t.ctx, "some-label").unwrap()); - let msg_id2 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).unwrap(); + assert!(was_device_msg_ever_added(&t.ctx, "some-label") + .await + .unwrap()); + let msg_id2 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); assert!(msg_id2.is_unset()); // ... unless everything is deleted and resetted - as needed eg. on device switch - delete_and_reset_all_device_msgs(&t.ctx).unwrap(); - assert!(!was_device_msg_ever_added(&t.ctx, "some-label").unwrap()); - let msg_id3 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).unwrap(); + delete_and_reset_all_device_msgs(&t.ctx).await.unwrap(); + assert!(!was_device_msg_ever_added(&t.ctx, "some-label") + .await + .unwrap()); + let msg_id3 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)) + .await + .unwrap(); assert_ne!(msg_id1, msg_id3); } - fn chatlist_len(ctx: &Context, listflags: usize) -> usize { + async fn chatlist_len(ctx: &Context, listflags: usize) -> usize { Chatlist::try_load(ctx, listflags, None, None) + .await .unwrap() .len() } - #[test] - fn test_archive() { + #[async_std::test] + async fn test_archive() { // create two chats - let t = dummy_context(); + let t = dummy_context().await; let mut msg = Message::new(Viewtype::Text); msg.text = Some("foo".to_string()); - let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); + let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).await.unwrap(); let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id) + .await .unwrap() .chat_id; - let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); + let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); assert!(!chat_id1.is_special()); assert!(!chat_id2.is_special()); - assert_eq!(get_chat_cnt(&t.ctx), 2); - assert_eq!(chatlist_len(&t.ctx, 0), 2); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 2); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 0); + assert_eq!(get_chat_cnt(&t.ctx).await, 2); + assert_eq!(chatlist_len(&t.ctx, 0).await, 2); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS).await, 2); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY).await, 0); assert_eq!(DC_GCL_ARCHIVED_ONLY, 0x01); assert_eq!(DC_GCL_NO_SPECIALS, 0x02); // archive first chat assert!(chat_id1 .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .is_ok()); assert!( Chat::load_from_db(&t.ctx, chat_id1) + .await .unwrap() .get_visibility() == ChatVisibility::Archived ); assert!( Chat::load_from_db(&t.ctx, chat_id2) + .await .unwrap() .get_visibility() == ChatVisibility::Normal ); - assert_eq!(get_chat_cnt(&t.ctx), 2); - assert_eq!(chatlist_len(&t.ctx, 0), 2); // including DC_CHAT_ID_ARCHIVED_LINK now - assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1); + assert_eq!(get_chat_cnt(&t.ctx).await, 2); + assert_eq!(chatlist_len(&t.ctx, 0).await, 2); // including DC_CHAT_ID_ARCHIVED_LINK now + assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS).await, 1); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY).await, 1); // archive second chat assert!(chat_id2 .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .is_ok()); assert!( Chat::load_from_db(&t.ctx, chat_id1) + .await .unwrap() .get_visibility() == ChatVisibility::Archived ); assert!( Chat::load_from_db(&t.ctx, chat_id2) + .await .unwrap() .get_visibility() == ChatVisibility::Archived ); - assert_eq!(get_chat_cnt(&t.ctx), 2); - assert_eq!(chatlist_len(&t.ctx, 0), 1); // only DC_CHAT_ID_ARCHIVED_LINK now - assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 0); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 2); + assert_eq!(get_chat_cnt(&t.ctx).await, 2); + assert_eq!(chatlist_len(&t.ctx, 0).await, 1); // only DC_CHAT_ID_ARCHIVED_LINK now + assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS).await, 0); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY).await, 2); // archive already archived first chat, unarchive second chat two times assert!(chat_id1 .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .is_ok()); assert!(chat_id2 .set_visibility(&t.ctx, ChatVisibility::Normal) + .await .is_ok()); assert!(chat_id2 .set_visibility(&t.ctx, ChatVisibility::Normal) + .await .is_ok()); assert!( Chat::load_from_db(&t.ctx, chat_id1) + .await .unwrap() .get_visibility() == ChatVisibility::Archived ); assert!( Chat::load_from_db(&t.ctx, chat_id2) + .await .unwrap() .get_visibility() == ChatVisibility::Normal ); - assert_eq!(get_chat_cnt(&t.ctx), 2); - assert_eq!(chatlist_len(&t.ctx, 0), 2); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1); - assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1); + assert_eq!(get_chat_cnt(&t.ctx).await, 2); + assert_eq!(chatlist_len(&t.ctx, 0).await, 2); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS).await, 1); + assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY).await, 1); } - fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { - let chatlist = Chatlist::try_load(ctx, listflags, None, None).unwrap(); + async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { + let chatlist = Chatlist::try_load(ctx, listflags, None, None) + .await + .unwrap(); let mut result = Vec::new(); for chatlist_index in 0..chatlist.len() { result.push(chatlist.get_chat_id(chatlist_index)) @@ -2961,125 +3253,166 @@ mod tests { result } - #[test] - fn test_pinned() { - let t = dummy_context(); + #[async_std::test] + async fn test_pinned() { + let t = dummy_context().await; // create 3 chats, wait 1 second in between to get a reliable order (we order by time) let mut msg = Message::new(Viewtype::Text); msg.text = Some("foo".to_string()); - let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); + let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).await.unwrap(); let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id) + .await .unwrap() .chat_id; - std::thread::sleep(std::time::Duration::from_millis(1000)); - let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); - std::thread::sleep(std::time::Duration::from_millis(1000)); - let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); + async_std::task::sleep(std::time::Duration::from_millis(1000)).await; + let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); + async_std::task::sleep(std::time::Duration::from_millis(1000)).await; + let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); - let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS); + let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS).await; assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); // pin assert!(chat_id1 .set_visibility(&t.ctx, ChatVisibility::Pinned) + .await .is_ok()); assert_eq!( Chat::load_from_db(&t.ctx, chat_id1) + .await .unwrap() .get_visibility(), ChatVisibility::Pinned ); // check if chat order changed - let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS); + let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS).await; assert_eq!(chatlist, vec![chat_id1, chat_id3, chat_id2]); // unpin assert!(chat_id1 .set_visibility(&t.ctx, ChatVisibility::Normal) + .await .is_ok()); assert_eq!( Chat::load_from_db(&t.ctx, chat_id1) + .await .unwrap() .get_visibility(), ChatVisibility::Normal ); // check if chat order changed back - let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS); + let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS).await; assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); } - #[test] - fn test_set_chat_name() { - let t = dummy_context(); - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); + #[async_std::test] + async fn test_set_chat_name() { + let t = dummy_context().await; + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().get_name(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .get_name(), "foo" ); - set_chat_name(&t.ctx, chat_id, "bar").unwrap(); + set_chat_name(&t.ctx, chat_id, "bar").await.unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().get_name(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .get_name(), "bar" ); } - #[test] - fn test_create_same_chat_twice() { - let context = dummy_context(); - let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap(); + #[async_std::test] + async fn test_create_same_chat_twice() { + let context = dummy_context().await; + let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de") + .await + .unwrap(); assert_ne!(contact1, 0); - let chat_id = create_by_contact_id(&context.ctx, contact1).unwrap(); + let chat_id = create_by_contact_id(&context.ctx, contact1).await.unwrap(); assert!(!chat_id.is_special(), "chat_id too small {}", chat_id); - let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap(); + let chat = Chat::load_from_db(&context.ctx, chat_id).await.unwrap(); - let chat2_id = create_by_contact_id(&context.ctx, contact1).unwrap(); + let chat2_id = create_by_contact_id(&context.ctx, contact1).await.unwrap(); assert_eq!(chat2_id, chat_id); - let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap(); + let chat2 = Chat::load_from_db(&context.ctx, chat2_id).await.unwrap(); assert_eq!(chat2.name, chat.name); } - #[test] - fn test_shall_attach_selfavatar() { - let t = dummy_context(); - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); - assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + #[async_std::test] + async fn test_shall_attach_selfavatar() { + let t = dummy_context().await; + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); + assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); let (contact_id, _) = - Contact::add_or_lookup(&t.ctx, "", "foo@bar.org", Origin::IncomingUnknownTo).unwrap(); - add_contact_to_chat(&t.ctx, chat_id, contact_id); - assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); - t.ctx.set_config(Config::Selfavatar, None).unwrap(); // setting to None also forces re-sending - assert!(shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + Contact::add_or_lookup(&t.ctx, "", "foo@bar.org", Origin::IncomingUnknownTo) + .await + .unwrap(); + add_contact_to_chat(&t.ctx, chat_id, contact_id).await; + assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); + t.ctx.set_config(Config::Selfavatar, None).await.unwrap(); // setting to None also forces re-sending + assert!(shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); - assert!(chat_id.set_selfavatar_timestamp(&t.ctx, time()).is_ok()); - assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + assert!(chat_id + .set_selfavatar_timestamp(&t.ctx, time()) + .await + .is_ok()); + assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap()); } - #[test] - fn test_set_mute_duration() { - let t = dummy_context(); - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); + #[async_std::test] + async fn test_set_mute_duration() { + let t = dummy_context().await; + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); // Initial assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .is_muted(), false ); // Forever - set_muted(&t.ctx, chat_id, MuteDuration::Forever).unwrap(); + set_muted(&t.ctx, chat_id, MuteDuration::Forever) + .await + .unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .is_muted(), true ); // unMute - set_muted(&t.ctx, chat_id, MuteDuration::NotMuted).unwrap(); + set_muted(&t.ctx, chat_id, MuteDuration::NotMuted) + .await + .unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .is_muted(), false ); // Timed in the future @@ -3088,9 +3421,13 @@ mod tests { chat_id, MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)), ) + .await .unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .is_muted(), true ); // Time in the past @@ -3099,22 +3436,28 @@ mod tests { chat_id, MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)), ) + .await .unwrap(); assert_eq!( - Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(), + Chat::load_from_db(&t.ctx, chat_id) + .await + .unwrap() + .is_muted(), false ); } - #[test] - fn test_parent_is_encrypted() { - let t = dummy_context(); - let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); - assert!(!chat_id.parent_is_encrypted(&t.ctx).unwrap()); + #[async_std::test] + async fn test_parent_is_encrypted() { + let t = dummy_context().await; + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); + assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap()); let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("hello".to_string())); - chat_id.set_draft(&t.ctx, Some(&mut msg)); - assert!(!chat_id.parent_is_encrypted(&t.ctx).unwrap()); + chat_id.set_draft(&t.ctx, Some(&mut msg)).await; + assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap()); } } diff --git a/src/chatlist.rs b/src/chatlist.rs index bf88c79ee..bd478eb2b 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -86,7 +86,7 @@ impl Chatlist { /// are returned. /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID /// are returned. - pub fn try_load( + pub async fn try_load( context: &Context, listflags: usize, query: Option<&str>, @@ -99,7 +99,7 @@ impl Chatlist { // Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some // messages get deleted to avoid reloading the same chatlist. - if let Err(err) = delete_device_expired_messages(context) { + if let Err(err) = delete_device_expired_messages(context).await { warn!(context, "Failed to hide expired messages: {}", err); } @@ -118,6 +118,7 @@ impl Chatlist { let skip_id = if flag_for_forwarding { chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) + .await .unwrap_or_default() .0 } else { @@ -156,17 +157,19 @@ impl Chatlist { AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2) GROUP BY c.id ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned], + paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned], process_row, process_rows, - )? + ).await? } else if flag_archived_only { // show archived chats // (this includes the archived device-chat; we could skip it, // however, then the number of archived chats do not match, which might be even more irritating. // and adapting the number requires larger refactorings and seems not to be worth the effort) - context.sql.query_map( - "SELECT c.id, m.id + context + .sql + .query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -180,23 +183,26 @@ impl Chatlist { AND c.archived=1 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - params![MessageState::OutDraft], - process_row, - process_rows, - )? + paramsv![MessageState::OutDraft], + process_row, + process_rows, + ) + .await? } else if let Some(query) = query { let query = query.trim().to_string(); ensure!(!query.is_empty(), "missing query"); // allow searching over special names that may change at any time // when the ui calls set_stock_translation() - if let Err(err) = update_special_chat_names(context) { + if let Err(err) = update_special_chat_names(context).await { warn!(context, "cannot update special chat names: {:?}", err) } let str_like_cmd = format!("%{}%", query); - context.sql.query_map( - "SELECT c.id, m.id + context + .sql + .query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -210,14 +216,16 @@ impl Chatlist { AND c.name LIKE ?3 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - params![MessageState::OutDraft, skip_id, str_like_cmd], - process_row, - process_rows, - )? + paramsv![MessageState::OutDraft, skip_id, str_like_cmd], + process_row, + process_rows, + ) + .await? } else { // show normal chatlist let sort_id_up = if flag_for_forwarding { chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF) + .await .unwrap_or_default() .0 } else { @@ -238,12 +246,13 @@ impl Chatlist { 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;", - params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], + paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], process_row, process_rows, - )?; + ).await?; if !flag_no_specials { - if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) { + if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await + { if !flag_for_forwarding { ids.insert( 0, @@ -256,7 +265,7 @@ impl Chatlist { ids }; - if add_archived_link_item && dc_get_archived_cnt(context) > 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))); } @@ -310,7 +319,7 @@ impl Chatlist { /// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable. /// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). // 0 if not applicable. - pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot { + pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot { // The summary is created by the chat, not by the last message. // This is because we may want to display drafts here or stuff as // "is typing". @@ -328,7 +337,7 @@ impl Chatlist { let chat_loaded: Chat; let chat = if let Some(chat) = chat { chat - } else if let Ok(chat) = Chat::load_from_db(context, *chat_id) { + } else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await { chat_loaded = chat; &chat_loaded } else { @@ -337,11 +346,11 @@ impl Chatlist { let mut lastcontact = None; - let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id) { + let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await { if lastmsg.from_id != DC_CONTACT_ID_SELF && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) { - lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok(); + lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok(); } Some(lastmsg) @@ -353,9 +362,15 @@ impl Chatlist { ret.text2 = None; } else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED { - ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string()); + ret.text2 = Some( + context + .stock_str(StockMessage::NoMessages) + .await + .to_string(), + ); } else { - ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context); + ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context) + .await; } ret @@ -367,34 +382,38 @@ impl Chatlist { } /// Returns the number of archived chats -pub fn dc_get_archived_cnt(context: &Context) -> u32 { +pub async fn dc_get_archived_cnt(context: &Context) -> u32 { context .sql .query_get_value( context, "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", - params![], + paramsv![], ) + .await .unwrap_or_default() } -fn get_last_deaddrop_fresh_msg(context: &Context) -> Option { +async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option { // We have an index over the state-column, this should be // sufficient as there are typically only few fresh messages. - context.sql.query_get_value( - context, - concat!( - "SELECT m.id", - " FROM msgs m", - " LEFT JOIN chats c", - " ON c.id=m.chat_id", - " WHERE m.state=10", - " AND m.hidden=0", - " AND c.blocked=2", - " ORDER BY m.timestamp DESC, m.id DESC;" - ), - params![], - ) + 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 } #[cfg(test)] @@ -403,15 +422,21 @@ mod tests { use crate::test_utils::*; - #[test] - fn test_try_load() { - let t = dummy_context(); - let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); - let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap(); - let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap(); + #[async_std::test] + async fn test_try_load() { + let t = dummy_context().await; + let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + .await + .unwrap(); + let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat") + .await + .unwrap(); + let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat") + .await + .unwrap(); // check that the chatlist starts with the most recent message - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 3); assert_eq!(chats.get_chat_id(0), chat_id3); assert_eq!(chats.get_chat_id(1), chat_id2); @@ -420,77 +445,102 @@ mod tests { // drafts are sorted to the top let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("hello".to_string())); - chat_id2.set_draft(&t.ctx, Some(&mut msg)); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + chat_id2.set_draft(&t.ctx, Some(&mut msg)).await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.get_chat_id(0), chat_id2); // check chatlist query and archive functionality - let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None) + .await + .unwrap(); assert_eq!(chats.len(), 1); - let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None) + .await + .unwrap(); assert_eq!(chats.len(), 0); chat_id1 .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .ok(); - let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None) + .await + .unwrap(); assert_eq!(chats.len(), 1); } - #[test] - fn test_sort_self_talk_up_on_forward() { - let t = dummy_context(); - t.ctx.update_device_chats().unwrap(); - create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); + #[async_std::test] + async fn test_sort_self_talk_up_on_forward() { + let t = dummy_context().await; + t.ctx.update_device_chats().await.unwrap(); + create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + .await + .unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert!(chats.len() == 3); assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0)) + .await .unwrap() .is_self_talk()); - let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None) + .await + .unwrap(); assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0)) + .await .unwrap() .is_self_talk()); } - #[test] - fn test_search_special_chat_names() { - let t = dummy_context(); - t.ctx.update_device_chats().unwrap(); + #[async_std::test] + async fn test_search_special_chat_names() { + let t = dummy_context().await; + t.ctx.update_device_chats().await.unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None) + .await + .unwrap(); assert_eq!(chats.len(), 0); - let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None) + .await + .unwrap(); assert_eq!(chats.len(), 0); t.ctx .set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string()) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None) + .await .unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap(); assert_eq!(chats.len(), 1); t.ctx .set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string()) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None) + .await .unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap(); assert_eq!(chats.len(), 1); } - #[test] - fn test_get_summary_unwrap() { - let t = dummy_context(); - let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); + #[async_std::test] + async fn test_get_summary_unwrap() { + let t = dummy_context().await; + let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat") + .await + .unwrap(); let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("foo:\nbar \r\n test".to_string())); - chat_id1.set_draft(&t.ctx, Some(&mut msg)); + chat_id1.set_draft(&t.ctx, Some(&mut msg)).await; - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); - let summary = chats.get_summary(&t.ctx, 0, None); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + let summary = chats.get_summary(&t.ctx, 0, None).await; assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary } } diff --git a/src/config.rs b/src/config.rs index 7abf10b2f..55c465155 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,11 +9,9 @@ use crate::constants::DC_VERSION_STR; use crate::context::Context; use crate::dc_tools::*; use crate::events::Event; -use crate::job::*; use crate::message::MsgId; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::stock::StockMessage; -use rusqlite::NO_PARAMS; /// The available configuration keys. #[derive( @@ -120,16 +118,16 @@ pub enum Config { impl Context { /// Get a configuration key. Returns `None` if no value is set, and no default value found. - pub fn get_config(&self, key: Config) -> Option { + pub async fn get_config(&self, key: Config) -> Option { let value = match key { Config::Selfavatar => { - let rel_path = self.sql.get_raw_config(self, key); + let rel_path = self.sql.get_raw_config(self, 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), + _ => self.sql.get_raw_config(self, key).await, }; if value.is_some() { @@ -138,27 +136,28 @@ impl Context { // Default values match key { - Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).into_owned()), + Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()), _ => key.get_str("default").map(|s| s.to_string()), } } - pub fn get_config_int(&self, key: Config) -> i32 { + pub async fn get_config_int(&self, key: Config) -> i32 { self.get_config(key) + .await .and_then(|s| s.parse().ok()) .unwrap_or_default() } - pub fn get_config_bool(&self, key: Config) -> bool { - self.get_config_int(key) != 0 + pub async fn get_config_bool(&self, key: Config) -> bool { + self.get_config_int(key).await != 0 } /// Gets configured "delete_server_after" value. /// /// `None` means never delete the message, `Some(0)` means delete /// at once, `Some(x)` means delete after `x` seconds. - pub fn get_config_delete_server_after(&self) -> Option { - match self.get_config_int(Config::DeleteServerAfter) { + pub async fn get_config_delete_server_after(&self) -> Option { + match self.get_config_int(Config::DeleteServerAfter).await { 0 => None, 1 => Some(0), x => Some(x as i64), @@ -169,8 +168,8 @@ impl Context { /// /// `None` means never delete the message, `Some(x)` means delete /// after `x` seconds. - pub fn get_config_delete_device_after(&self) -> Option { - match self.get_config_int(Config::DeleteDeviceAfter) { + pub async fn get_config_delete_device_after(&self) -> Option { + match self.get_config_int(Config::DeleteDeviceAfter).await { 0 => None, x => Some(x as i64), } @@ -178,57 +177,61 @@ impl Context { /// 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 fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> { + pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> { match key { Config::Selfavatar => { self.sql - .execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?; + .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![]) + .await?; self.sql - .set_raw_config_bool(self, "attach_selfavatar", true)?; + .set_raw_config_bool(self, "attach_selfavatar", true) + .await?; match value { Some(value) => { - let blob = BlobObject::new_from_path(&self, value)?; + let blob = BlobObject::new_from_path(&self, value).await?; blob.recode_to_avatar_size(self)?; - self.sql.set_raw_config(self, key, Some(blob.as_name())) + self.sql + .set_raw_config(self, key, Some(blob.as_name())) + .await } - None => self.sql.set_raw_config(self, key, None), + None => self.sql.set_raw_config(self, key, None).await, } } Config::InboxWatch => { - let ret = self.sql.set_raw_config(self, key, value); - interrupt_inbox_idle(self); + let ret = self.sql.set_raw_config(self, key, value).await; + self.interrupt_inbox(false).await; ret } Config::SentboxWatch => { - let ret = self.sql.set_raw_config(self, key, value); - interrupt_sentbox_idle(self); + let ret = self.sql.set_raw_config(self, key, value).await; + self.interrupt_sentbox(false).await; ret } Config::MvboxWatch => { - let ret = self.sql.set_raw_config(self, key, value); - interrupt_mvbox_idle(self); + let ret = self.sql.set_raw_config(self, key, value).await; + self.interrupt_mvbox(false).await; ret } Config::Selfstatus => { - let def = self.stock_str(StockMessage::StatusLine); + let def = self.stock_str(StockMessage::StatusLine).await; let val = if value.is_none() || value.unwrap() == def { None } else { value }; - self.sql.set_raw_config(self, key, val) + self.sql.set_raw_config(self, key, val).await } Config::DeleteDeviceAfter => { - let ret = self.sql.set_raw_config(self, key, value); + let ret = self.sql.set_raw_config(self, key, value).await; // Force chatlist reload to delete old messages immediately. - self.call_cb(Event::MsgsChanged { + self.emit_event(Event::MsgsChanged { msg_id: MsgId::new(0), chat_id: ChatId::new(0), }); ret } - _ => self.sql.set_raw_config(self, key, value), + _ => self.sql.set_raw_config(self, key, value).await, } } } @@ -276,9 +279,9 @@ mod tests { assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX")); } - #[test] - fn test_selfavatar_outside_blobdir() { - let t = dummy_context(); + #[async_std::test] + async fn test_selfavatar_outside_blobdir() { + let t = dummy_context().await; let avatar_src = t.dir.path().join("avatar.jpg"); let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); File::create(&avatar_src) @@ -286,13 +289,14 @@ mod tests { .write_all(avatar_bytes) .unwrap(); let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg"); - assert!(!avatar_blob.exists()); + assert!(!avatar_blob.exists().await); t.ctx .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) + .await .unwrap(); - assert!(avatar_blob.exists()); + assert!(avatar_blob.exists().await); assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64); - let avatar_cfg = t.ctx.get_config(Config::Selfavatar); + let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await; assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); let img = image::open(avatar_src).unwrap(); @@ -304,9 +308,9 @@ mod tests { assert_eq!(img.height(), AVATAR_SIZE); } - #[test] - fn test_selfavatar_in_blobdir() { - let t = dummy_context(); + #[async_std::test] + async fn test_selfavatar_in_blobdir() { + let t = dummy_context().await; let avatar_src = t.ctx.get_blobdir().join("avatar.png"); let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png"); File::create(&avatar_src) @@ -320,8 +324,9 @@ mod tests { t.ctx .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) + .await .unwrap(); - let avatar_cfg = t.ctx.get_config(Config::Selfavatar); + let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await; assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string())); let img = image::open(avatar_src).unwrap(); @@ -329,9 +334,9 @@ mod tests { assert_eq!(img.height(), AVATAR_SIZE); } - #[test] - fn test_selfavatar_copy_without_recode() { - let t = dummy_context(); + #[async_std::test] + async fn test_selfavatar_copy_without_recode() { + let t = dummy_context().await; let avatar_src = t.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); File::create(&avatar_src) @@ -339,30 +344,34 @@ mod tests { .write_all(avatar_bytes) .unwrap(); let avatar_blob = t.ctx.get_blobdir().join("avatar.png"); - assert!(!avatar_blob.exists()); + assert!(!avatar_blob.exists().await); t.ctx .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) + .await .unwrap(); - assert!(avatar_blob.exists()); + assert!(avatar_blob.exists().await); assert_eq!( std::fs::metadata(&avatar_blob).unwrap().len(), avatar_bytes.len() as u64 ); - let avatar_cfg = t.ctx.get_config(Config::Selfavatar); + let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await; assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); } - #[test] - fn test_media_quality_config_option() { - let t = dummy_context(); - let media_quality = t.ctx.get_config_int(Config::MediaQuality); + #[async_std::test] + async fn test_media_quality_config_option() { + let t = dummy_context().await; + let media_quality = t.ctx.get_config_int(Config::MediaQuality).await; assert_eq!(media_quality, 0); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); assert_eq!(media_quality, constants::MediaQuality::Balanced); - t.ctx.set_config(Config::MediaQuality, Some("1")).unwrap(); + t.ctx + .set_config(Config::MediaQuality, Some("1")) + .await + .unwrap(); - let media_quality = t.ctx.get_config_int(Config::MediaQuality); + let media_quality = t.ctx.get_config_int(Config::MediaQuality).await; assert_eq!(media_quality, 1); assert_eq!(constants::MediaQuality::Worse as i32, 1); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index dbadc9543..984012924 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -94,12 +94,12 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result { } } -pub fn moz_autoconfigure( +pub async fn moz_autoconfigure( context: &Context, url: &str, param_in: &LoginParam, ) -> Result { - let xml_raw = read_url(context, url)?; + let xml_raw = read_url(context, url).await?; let res = parse_xml(¶m_in.addr, &xml_raw); if let Err(err) = &res { diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 96b22e2ab..638ae2505 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -112,7 +112,7 @@ fn parse_xml(xml_raw: &str) -> Result { Ok(res) } -pub fn outlk_autodiscover( +pub async fn outlk_autodiscover( context: &Context, url: &str, _param_in: &LoginParam, @@ -120,7 +120,7 @@ pub fn outlk_autodiscover( let mut url = url.to_string(); /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */ for _i in 0..10 { - let xml_raw = read_url(context, &url)?; + let xml_raw = read_url(context, &url).await?; let res = parse_xml(&xml_raw); if let Err(err) = &res { warn!(context, "{}", err); diff --git a/src/configure/mod.rs b/src/configure/mod.rs index 49ec6e299..602c316ce 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -4,22 +4,21 @@ mod auto_mozilla; mod auto_outlook; mod read_url; +use anyhow::{bail, ensure, format_err, Result}; +use async_std::prelude::*; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; -use async_std::task; - use crate::config::Config; use crate::constants::*; use crate::context::Context; use crate::dc_tools::*; -use crate::error::format_err; -use crate::job::{self, job_add, job_kill_action}; +use crate::imap::Imap; use crate::login_param::{CertificateChecks, LoginParam}; +use crate::message::Message; use crate::oauth2::*; -use crate::param::Params; +use crate::smtp::Smtp; use crate::{chat, e2ee, provider}; -use crate::message::Message; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; @@ -29,440 +28,441 @@ macro_rules! progress { $progress <= 1000, "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" ); - $context.call_cb($crate::events::Event::ConfigureProgress($progress)); + $context.emit_event($crate::events::Event::ConfigureProgress($progress)); }; } impl Context { - /// Starts a configuration job. - pub fn configure(&self) { - if self.has_ongoing() { - warn!(self, "There is already another ongoing process running.",); - return; - } - job_kill_action(self, job::Action::ConfigureImap); - job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0); + /// Checks if the context is already configured. + pub async fn is_configured(&self) -> bool { + self.sql.get_raw_config_bool(self, "configured").await } - /// Checks if the context is already configured. - pub fn is_configured(&self) -> bool { - self.sql.get_raw_config_bool(self, "configured") + /// Configures this account with the currently set parameters. + pub async fn configure(&self) -> Result<()> { + use futures::future::FutureExt; + + ensure!( + !self.scheduler.read().await.is_running(), + "cannot configure, already running" + ); + ensure!( + self.sql.is_open().await, + "cannot configure, database not opened." + ); + let cancel_channel = self.alloc_ongoing().await?; + + let res = self + .inner_configure() + .race(cancel_channel.recv().map(|_| { + progress!(self, 0); + Ok(()) + })) + .await; + + self.free_ongoing().await; + + res + } + + async fn inner_configure(&self) -> Result<()> { + let mut success = false; + let mut param_autoconfig: Option = None; + + info!(self, "Configure ..."); + + // Variables that are shared between steps: + let mut param = LoginParam::from_database(self, "").await; + // need all vars here to be mutable because rust thinks the same step could be called multiple times + // and also initialize, because otherwise rust thinks it's used while unitilized, even if thats not the case as the loop goes only forward + let mut param_domain = "undefined.undefined".to_owned(); + let mut param_addr_urlencoded: String = + "Internal Error: this value should never be used".to_owned(); + let mut keep_flags = 0; + + let mut step_counter: u8 = 0; + let (_s, r) = async_std::sync::channel(1); + let mut imap = Imap::new(r); + let was_configured_before = self.is_configured().await; + + while !self.shall_stop_ongoing().await { + step_counter += 1; + + match exec_step( + self, + &mut imap, + &mut param, + &mut param_domain, + &mut param_autoconfig, + &mut param_addr_urlencoded, + &mut keep_flags, + &mut step_counter, + ) + .await + { + Ok(step) => { + success = true; + match step { + Step::Continue => {} + Step::Done => break, + } + } + Err(err) => { + error!(self, "{}", err); + success = false; + break; + } + } + } + + if imap.is_connected() { + imap.disconnect(self).await; + } + + // remember the entered parameters on success + // and restore to last-entered on failure. + // this way, the parameters visible to the ui are always in-sync with the current configuration. + if success { + LoginParam::from_database(self, "") + .await + .save_to_database(self, "configured_raw_") + .await + .ok(); + } else { + LoginParam::from_database(self, "configured_raw_") + .await + .save_to_database(self, "") + .await + .ok(); + } + + if let Some(provider) = provider::get_provider_info(¶m.addr) { + if !was_configured_before { + if let Some(config_defaults) = &provider.config_defaults { + for def in config_defaults.iter() { + info!(self, "apply config_defaults {}={}", def.key, def.value); + self.set_config(def.key, Some(def.value)).await?; + } + } + } + + if !provider.after_login_hint.is_empty() { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(provider.after_login_hint.to_string()); + if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg)) + .await + .is_err() + { + warn!(self, "cannot add after_login_hint as core-provider-info"); + } + } + } + + if success { + progress!(self, 1000); + Ok(()) + } else { + progress!(self, 0); + Err(format_err!("Configure failed")) + } } } -/******************************************************************************* - * Configure JOB - ******************************************************************************/ -#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)] -pub(crate) fn JobConfigureImap(context: &Context) -> job::Status { - if !context.sql.is_open() { - error!(context, "Cannot configure, database not opened.",); - progress!(context, 0); - return job::Status::Finished(Err(format_err!("Database not opened"))); - } - if !context.alloc_ongoing() { - progress!(context, 0); - return job::Status::Finished(Err(format_err!("Cannot allocated ongoing process"))); - } - let mut success = false; - let mut imap_connected_here = false; - let mut smtp_connected_here = false; - - let mut param_autoconfig: Option = None; - let was_configured_before = context.is_configured(); - - context - .inbox_thread - .read() - .unwrap() - .imap - .disconnect(context); - context - .sentbox_thread - .read() - .unwrap() - .imap - .disconnect(context); - context - .mvbox_thread - .read() - .unwrap() - .imap - .disconnect(context); - context.smtp.clone().lock().unwrap().disconnect(); - info!(context, "Configure ...",); - - // Variables that are shared between steps: - let mut param = LoginParam::from_database(context, ""); - // need all vars here to be mutable because rust thinks the same step could be called multiple times - // and also initialize, because otherwise rust thinks it's used while unitilized, even if thats not the case as the loop goes only forward - let mut param_domain = "undefined.undefined".to_owned(); - let mut param_addr_urlencoded: String = - "Internal Error: this value should never be used".to_owned(); - let mut keep_flags = 0; - +#[allow(clippy::too_many_arguments)] +async fn exec_step( + ctx: &Context, + imap: &mut Imap, + param: &mut LoginParam, + param_domain: &mut String, + param_autoconfig: &mut Option, + param_addr_urlencoded: &mut String, + keep_flags: &mut i32, + step_counter: &mut u8, +) -> Result { const STEP_12_USE_AUTOCONFIG: u8 = 12; const STEP_13_AFTER_AUTOCONFIG: u8 = 13; - let mut step_counter: u8 = 0; - while !context.shall_stop_ongoing() { - step_counter += 1; - - let success = match step_counter { - // Read login parameters from the database - 1 => { - progress!(context, 1); - if param.addr.is_empty() { - error!(context, "Please enter an email address.",); - } - !param.addr.is_empty() - } - // Step 1: Load the parameters and check email-address and password - 2 => { - if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 { - // the used oauth2 addr may differ, check this. - // if dc_get_oauth2_addr() is not available in the oauth2 implementation, - // just use the given one. - progress!(context, 10); - if let Some(oauth2_addr) = - dc_get_oauth2_addr(context, ¶m.addr, ¶m.mail_pw) - .and_then(|e| e.parse().ok()) - { - info!(context, "Authorized address is {}", oauth2_addr); - param.addr = oauth2_addr; - context - .sql - .set_raw_config(context, "addr", Some(param.addr.as_str())) - .ok(); - } - progress!(context, 20); - } - true // no oauth? - just continue it's no error - } - 3 => { - if let Ok(parsed) = param.addr.parse() { - let parsed: EmailAddress = parsed; - param_domain = parsed.domain; - param_addr_urlencoded = - utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string(); - true - } else { - error!(context, "Bad email-address."); - false - } - } - // Step 2: Autoconfig - 4 => { - progress!(context, 200); - - if param.mail_server.is_empty() - && param.mail_port == 0 - /*&¶m.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */ - && param.send_server.is_empty() - && param.send_port == 0 - && param.send_user.is_empty() - /*&¶m.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */ - && (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0 + match *step_counter { + // Read login parameters from the database + 1 => { + progress!(ctx, 1); + ensure!(!param.addr.is_empty(), "Please enter an email address."); + } + // Step 1: Load the parameters and check email-address and password + 2 => { + if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 { + // the used oauth2 addr may differ, check this. + // if dc_get_oauth2_addr() is not available in the oauth2 implementation, + // just use the given one. + progress!(ctx, 10); + if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.mail_pw) + .await + .and_then(|e| e.parse().ok()) { - // no advanced parameters entered by the user: query provider-database or do Autoconfig - keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2; - if let Some(new_param) = get_offline_autoconfig(context, ¶m) { - // got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting - param_autoconfig = Some(new_param); - step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop - } - } else { - // advanced parameters entered by the user: skip Autoconfig - step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop + info!(ctx, "Authorized address is {}", oauth2_addr); + param.addr = oauth2_addr; + ctx.sql + .set_raw_config(ctx, "addr", Some(param.addr.as_str())) + .await?; } - true + progress!(ctx, 20); } - /* A. Search configurations from the domain used in the email-address, prefer encrypted */ - 5 => { - if param_autoconfig.is_none() { - let url = format!( - "https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}", - param_domain, param_addr_urlencoded - ); - param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok(); - } - true - } - 6 => { - progress!(context, 300); - if param_autoconfig.is_none() { - // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense - let url = format!( - "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}", - param_domain, param_addr_urlencoded - ); - param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok(); - } - true - } - /* Outlook section start ------------- */ - /* Outlook uses always SSL but different domains (this comment describes the next two steps) */ - 7 => { - progress!(context, 310); - if param_autoconfig.is_none() { - let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain); - param_autoconfig = outlk_autodiscover(context, &url, ¶m).ok(); - } - true - } - 8 => { - progress!(context, 320); - if param_autoconfig.is_none() { - let url = format!( - "https://{}{}/autodiscover/autodiscover.xml", - "autodiscover.", param_domain - ); - param_autoconfig = outlk_autodiscover(context, &url, ¶m).ok(); - } - true - } - /* ----------- Outlook section end */ - 9 => { - progress!(context, 330); - if param_autoconfig.is_none() { - let url = format!( - "http://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}", - param_domain, param_addr_urlencoded - ); - param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok(); - } - true - } - 10 => { - progress!(context, 340); - if param_autoconfig.is_none() { - // do not transfer the email-address unencrypted - let url = format!( - "http://{}/.well-known/autoconfig/mail/config-v1.1.xml", - param_domain - ); - param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok(); - } - true - } - /* B. If we have no configuration yet, search configuration in Thunderbird's centeral database */ - 11 => { - progress!(context, 350); - if param_autoconfig.is_none() { - /* always SSL for Thunderbird's database */ - let url = format!("https://autoconfig.thunderbird.net/v1.1/{}", param_domain); - param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok(); - } - true - } - /* C. Do we have any autoconfig result? - If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above - */ - STEP_12_USE_AUTOCONFIG => { - progress!(context, 500); - if let Some(ref cfg) = param_autoconfig { - info!(context, "Got autoconfig: {}", &cfg); - if !cfg.mail_user.is_empty() { - param.mail_user = cfg.mail_user.clone(); - } - param.mail_server = cfg.mail_server.clone(); /* all other values are always NULL when entering autoconfig */ - param.mail_port = cfg.mail_port; - param.send_server = cfg.send_server.clone(); - param.send_port = cfg.send_port; - param.send_user = cfg.send_user.clone(); - param.server_flags = cfg.server_flags; - /* although param_autoconfig's data are no longer needed from, - it is used to later to prevent trying variations of port/server/logins */ - } - param.server_flags |= keep_flags; - true - } - // Step 3: Fill missing fields with defaults - // If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above - STEP_13_AFTER_AUTOCONFIG => { - if param.mail_server.is_empty() { - param.mail_server = format!("imap.{}", param_domain,) - } - if param.mail_port == 0 { - param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) { - 143 - } else { - 993 - } - } - if param.mail_user.is_empty() { - param.mail_user = param.addr.clone(); - } - if param.send_server.is_empty() && !param.mail_server.is_empty() { - param.send_server = param.mail_server.clone(); - if param.send_server.starts_with("imap.") { - param.send_server = param.send_server.replacen("imap", "smtp", 1); - } - } - if param.send_port == 0 { - param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 - { - 587 - } else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 { - 25 - } else { - 465 - } - } - if param.send_user.is_empty() && !param.mail_user.is_empty() { - param.send_user = param.mail_user.clone(); - } - if param.send_pw.is_empty() && !param.mail_pw.is_empty() { - param.send_pw = param.mail_pw.clone() - } - if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) { - param.server_flags &= !(DC_LP_AUTH_FLAGS as i32); - param.server_flags |= DC_LP_AUTH_NORMAL as i32 - } - if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) { - param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32); - param.server_flags |= if param.send_port == 143 { - DC_LP_IMAP_SOCKET_STARTTLS as i32 - } else { - DC_LP_IMAP_SOCKET_SSL as i32 - } - } - if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) { - param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32); - param.server_flags |= if param.send_port == 587 { - DC_LP_SMTP_SOCKET_STARTTLS as i32 - } else if param.send_port == 25 { - DC_LP_SMTP_SOCKET_PLAIN as i32 - } else { - DC_LP_SMTP_SOCKET_SSL as i32 - } - } - /* do we have a complete configuration? */ - if param.mail_server.is_empty() - || param.mail_port == 0 - || param.mail_user.is_empty() - || param.mail_pw.is_empty() - || param.send_server.is_empty() - || param.send_port == 0 - || param.send_user.is_empty() - || param.send_pw.is_empty() - || param.server_flags == 0 - { - error!(context, "Account settings incomplete."); - false - } else { - true - } - } - 14 => { - progress!(context, 600); - /* try to connect to IMAP - if we did not got an autoconfig, - do some further tries with different settings and username variations */ - imap_connected_here = - try_imap_connections(context, &mut param, param_autoconfig.is_some()); - imap_connected_here - } - 15 => { - progress!(context, 800); - smtp_connected_here = - try_smtp_connections(context, &mut param, param_autoconfig.is_some()); - smtp_connected_here - } - 16 => { - progress!(context, 900); - let create_mvbox = context.get_config_bool(Config::MvboxWatch) - || context.get_config_bool(Config::MvboxMove); - let imap = &context.inbox_thread.read().unwrap().imap; - if let Err(err) = imap.configure_folders(context, create_mvbox) { - warn!(context, "configuring folders failed: {:?}", err); - false - } else { - let res = imap.select_with_uidvalidity(context, "INBOX"); - if let Err(err) = res { - error!(context, "could not read INBOX status: {:?}", err); - false - } else { - true - } - } - } - 17 => { - progress!(context, 910); - /* configuration success - write back the configured parameters with the "configured_" prefix; also write the "configured"-flag */ - param - .save_to_database( - context, - "configured_", /*the trailing underscore is correct*/ - ) - .ok(); - - context.sql.set_raw_config_bool(context, "configured", true); - true - } - 18 => { - progress!(context, 920); - // we generate the keypair just now - we could also postpone this until the first message is sent, however, - // this may result in a unexpected and annoying delay when the user sends his very first message - // (~30 seconds on a Moto G4 play) and might looks as if message sending is always that slow. - e2ee::ensure_secret_key_exists(context); - success = true; - info!(context, "key generation completed"); - progress!(context, 940); - break; // We are done here - } - _ => { - error!(context, "Internal error: step counter out of bound",); - break; - } - }; - - if !success { - break; + // no oauth? - just continue it's no error } - } - if imap_connected_here { - context - .inbox_thread - .read() - .unwrap() - .imap - .disconnect(context); - } - if smtp_connected_here { - context.smtp.clone().lock().unwrap().disconnect(); - } - - // remember the entered parameters on success - // and restore to last-entered on failure. - // this way, the parameters visible to the ui are always in-sync with the current configuration. - if success { - LoginParam::from_database(context, "").save_to_database(context, "configured_raw_"); - } else { - LoginParam::from_database(context, "configured_raw_").save_to_database(context, ""); - } - - if let Some(provider) = provider::get_provider_info(¶m.addr) { - if !was_configured_before { - if let Some(config_defaults) = &provider.config_defaults { - for def in config_defaults.iter() { - info!(context, "apply config_defaults {}={}", def.key, def.value); - context.set_config(def.key, Some(def.value)); - } + 3 => { + if let Ok(parsed) = param.addr.parse() { + let parsed: EmailAddress = parsed; + *param_domain = parsed.domain; + *param_addr_urlencoded = + utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string(); + } else { + bail!("Bad email-address."); } } + // Step 2: Autoconfig + 4 => { + progress!(ctx, 200); - if !provider.after_login_hint.is_empty() { - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(provider.after_login_hint.to_string()); - if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() { - warn!(context, "cannot add after_login_hint as core-provider-info"); + if param.mail_server.is_empty() + && param.mail_port == 0 + /* && param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */ + && param.send_server.is_empty() + && param.send_port == 0 + && param.send_user.is_empty() + /* && param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */ + && (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0 + { + // no advanced parameters entered by the user: query provider-database or do Autoconfig + *keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2; + if let Some(new_param) = get_offline_autoconfig(ctx, ¶m) { + // got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting + *param_autoconfig = Some(new_param); + *step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop + } + } else { + // advanced parameters entered by the user: skip Autoconfig + *step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop } } + /* A. Search configurations from the domain used in the email-address, prefer encrypted */ + 5 => { + if param_autoconfig.is_none() { + let url = format!( + "https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}", + param_domain, param_addr_urlencoded + ); + *param_autoconfig = moz_autoconfigure(ctx, &url, ¶m).await.ok(); + } + } + 6 => { + progress!(ctx, 300); + if param_autoconfig.is_none() { + // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense + let url = format!( + "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}", + param_domain, param_addr_urlencoded + ); + *param_autoconfig = moz_autoconfigure(ctx, &url, ¶m).await.ok(); + } + } + /* Outlook section start ------------- */ + /* Outlook uses always SSL but different domains (this comment describes the next two steps) */ + 7 => { + progress!(ctx, 310); + if param_autoconfig.is_none() { + let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain); + *param_autoconfig = outlk_autodiscover(ctx, &url, ¶m).await.ok(); + } + } + 8 => { + progress!(ctx, 320); + if param_autoconfig.is_none() { + let url = format!( + "https://{}{}/autodiscover/autodiscover.xml", + "autodiscover.", param_domain + ); + *param_autoconfig = outlk_autodiscover(ctx, &url, ¶m).await.ok(); + } + } + /* ----------- Outlook section end */ + 9 => { + progress!(ctx, 330); + if param_autoconfig.is_none() { + let url = format!( + "http://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}", + param_domain, param_addr_urlencoded + ); + *param_autoconfig = moz_autoconfigure(ctx, &url, ¶m).await.ok(); + } + } + 10 => { + progress!(ctx, 340); + if param_autoconfig.is_none() { + // do not transfer the email-address unencrypted + let url = format!( + "http://{}/.well-known/autoconfig/mail/config-v1.1.xml", + param_domain + ); + *param_autoconfig = moz_autoconfigure(ctx, &url, ¶m).await.ok(); + } + } + /* B. If we have no configuration yet, search configuration in Thunderbird's centeral database */ + 11 => { + progress!(ctx, 350); + if param_autoconfig.is_none() { + /* always SSL for Thunderbird's database */ + let url = format!("https://autoconfig.thunderbird.net/v1.1/{}", param_domain); + *param_autoconfig = moz_autoconfigure(ctx, &url, ¶m).await.ok(); + } + } + /* C. Do we have any autoconfig result? + If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above + */ + STEP_12_USE_AUTOCONFIG => { + progress!(ctx, 500); + if let Some(ref cfg) = param_autoconfig { + info!(ctx, "Got autoconfig: {}", &cfg); + if !cfg.mail_user.is_empty() { + param.mail_user = cfg.mail_user.clone(); + } + param.mail_server = cfg.mail_server.clone(); /* all other values are always NULL when entering autoconfig */ + param.mail_port = cfg.mail_port; + param.send_server = cfg.send_server.clone(); + param.send_port = cfg.send_port; + param.send_user = cfg.send_user.clone(); + param.server_flags = cfg.server_flags; + /* although param_autoconfig's data are no longer needed from, + it is used to later to prevent trying variations of port/server/logins */ + } + param.server_flags |= *keep_flags; + } + // Step 3: Fill missing fields with defaults + // If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above + STEP_13_AFTER_AUTOCONFIG => { + if param.mail_server.is_empty() { + param.mail_server = format!("imap.{}", param_domain,) + } + if param.mail_port == 0 { + param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) { + 143 + } else { + 993 + } + } + if param.mail_user.is_empty() { + param.mail_user = param.addr.clone(); + } + if param.send_server.is_empty() && !param.mail_server.is_empty() { + param.send_server = param.mail_server.clone(); + if param.send_server.starts_with("imap.") { + param.send_server = param.send_server.replacen("imap", "smtp", 1); + } + } + if param.send_port == 0 { + param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 { + 587 + } else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 { + 25 + } else { + 465 + } + } + if param.send_user.is_empty() && !param.mail_user.is_empty() { + param.send_user = param.mail_user.clone(); + } + if param.send_pw.is_empty() && !param.mail_pw.is_empty() { + param.send_pw = param.mail_pw.clone() + } + if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) { + param.server_flags &= !(DC_LP_AUTH_FLAGS as i32); + param.server_flags |= DC_LP_AUTH_NORMAL as i32 + } + if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) { + param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32); + param.server_flags |= if param.send_port == 143 { + DC_LP_IMAP_SOCKET_STARTTLS as i32 + } else { + DC_LP_IMAP_SOCKET_SSL as i32 + } + } + if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) { + param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32); + param.server_flags |= if param.send_port == 587 { + DC_LP_SMTP_SOCKET_STARTTLS as i32 + } else if param.send_port == 25 { + DC_LP_SMTP_SOCKET_PLAIN as i32 + } else { + DC_LP_SMTP_SOCKET_SSL as i32 + } + } + /* do we have a complete configuration? */ + if param.mail_server.is_empty() + || param.mail_port == 0 + || param.mail_user.is_empty() + || param.mail_pw.is_empty() + || param.send_server.is_empty() + || param.send_port == 0 + || param.send_user.is_empty() + || param.send_pw.is_empty() + || param.server_flags == 0 + { + bail!("Account settings incomplete."); + } + } + 14 => { + progress!(ctx, 600); + /* try to connect to IMAP - if we did not got an autoconfig, + do some further tries with different settings and username variations */ + try_imap_connections(ctx, param, param_autoconfig.is_some(), imap).await?; + } + 15 => { + progress!(ctx, 800); + try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?; + } + 16 => { + progress!(ctx, 900); + + let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await + || ctx.get_config_bool(Config::MvboxMove).await; + + if let Err(err) = imap.configure_folders(ctx, create_mvbox).await { + bail!("configuring folders failed: {:?}", err); + } + + if let Err(err) = imap.select_with_uidvalidity(ctx, "INBOX").await { + bail!("could not read INBOX status: {:?}", err); + } + } + 17 => { + progress!(ctx, 910); + // configuration success - write back the configured parameters with the + // "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?; + } + 18 => { + progress!(ctx, 920); + // we generate the keypair just now - we could also postpone this until the first message is sent, however, + // this may result in a unexpected and annoying delay when the user sends his very first message + // (~30 seconds on a Moto G4 play) and might looks as if message sending is always that slow. + e2ee::ensure_secret_key_exists(ctx).await?; + info!(ctx, "key generation completed"); + progress!(ctx, 940); + return Ok(Step::Done); + } + _ => { + bail!("Internal error: step counter out of bound"); + } } - context.free_ongoing(); - progress!(context, if success { 1000 } else { 0 }); - job::Status::Finished(Ok(())) + Ok(Step::Continue) +} + +#[derive(Debug)] +enum Step { + Done, + Continue, } #[allow(clippy::unnecessary_unwrap)] @@ -523,14 +523,15 @@ fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option bool { + imap: &mut Imap, +) -> Result { // progress 650 and 660 - if let Some(res) = try_imap_connection(context, &mut param, was_autoconfig, 0) { - return res; + if let Ok(val) = try_imap_connection(context, &mut param, was_autoconfig, 0, imap).await { + return Ok(val); } progress!(context, 670); param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS); @@ -544,39 +545,38 @@ fn try_imap_connections( param.send_user = param.send_user.split_at(at).0.to_string(); } // progress 680 and 690 - if let Some(res) = try_imap_connection(context, &mut param, was_autoconfig, 1) { - res - } else { - false - } + try_imap_connection(context, &mut param, was_autoconfig, 1, imap).await } -fn try_imap_connection( +async fn try_imap_connection( context: &Context, param: &mut LoginParam, was_autoconfig: bool, variation: usize, -) -> Option { - if let Some(res) = try_imap_one_param(context, ¶m) { - return Some(res); + imap: &mut Imap, +) -> Result { + if try_imap_one_param(context, ¶m, imap).await.is_ok() { + return Ok(true); } if was_autoconfig { - return Some(false); + return Ok(false); } progress!(context, 650 + variation * 30); param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS); param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS; - if let Some(res) = try_imap_one_param(context, ¶m) { - return Some(res); + if try_imap_one_param(context, ¶m, imap).await.is_ok() { + return Ok(true); } progress!(context, 660 + variation * 30); param.mail_port = 143; - try_imap_one_param(context, ¶m) + try_imap_one_param(context, ¶m, imap).await?; + + Ok(true) } -fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option { +async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> { let inf = format!( "imap: {}@{}:{} flags=0x{:x} certificate_checks={}", param.mail_user, @@ -586,80 +586,63 @@ fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option { param.imap_certificate_checks ); info!(context, "Trying: {}", inf); - if task::block_on( - context - .inbox_thread - .read() - .unwrap() - .imap - .connect(context, ¶m), - ) { + + if imap.connect(context, ¶m).await { info!(context, "success: {}", inf); - return Some(true); + return Ok(()); } - if context.shall_stop_ongoing() { - return Some(false); + + if context.shall_stop_ongoing().await { + bail!("Interrupted"); } - info!(context, "Could not connect: {}", inf); - None + + bail!("Could not connect: {}", inf); } -fn try_smtp_connections( +async fn try_smtp_connections( context: &Context, mut param: &mut LoginParam, was_autoconfig: bool, -) -> bool { +) -> Result<()> { + let mut smtp = Smtp::new(); /* try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do a second try with STARTTLS-587 */ - if let Some(res) = try_smtp_one_param(context, ¶m) { - return res; + if try_smtp_one_param(context, ¶m, &mut smtp).await.is_ok() { + return Ok(()); } if was_autoconfig { - return false; + return Ok(()); } progress!(context, 850); param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32); param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32; param.send_port = 587; - if let Some(res) = try_smtp_one_param(context, ¶m) { - return res; + if try_smtp_one_param(context, ¶m, &mut smtp).await.is_ok() { + return Ok(()); } progress!(context, 860); param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32); param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32; param.send_port = 25; - if let Some(res) = try_smtp_one_param(context, ¶m) { - return res; - } - false + try_smtp_one_param(context, ¶m, &mut smtp).await?; + + Ok(()) } -fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option { +async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> { let inf = format!( "smtp: {}@{}:{} flags: 0x{:x}", param.send_user, param.send_server, param.send_port, param.server_flags ); info!(context, "Trying: {}", inf); - match context - .smtp - .clone() - .lock() - .unwrap() - .connect(context, ¶m) - { - Ok(()) => { - info!(context, "success: {}", inf); - Some(true) - } - Err(err) => { - if context.shall_stop_ongoing() { - Some(false) - } else { - warn!(context, "could not connect: {}", err); - None - } - } + + if let Err(err) = smtp.connect(context, ¶m).await { + bail!("could not connect: {}", err); } + + info!(context, "success: {}", inf); + smtp.disconnect().await; + Ok(()) } #[derive(Debug, thiserror::Error)] @@ -689,22 +672,25 @@ mod tests { use super::*; use crate::config::*; - use crate::configure::JobConfigureImap; use crate::test_utils::*; - #[test] - fn test_no_panic_on_bad_credentials() { - let t = dummy_context(); + #[async_std::test] + async fn test_no_panic_on_bad_credentials() { + let t = dummy_context().await; t.ctx .set_config(Config::Addr, Some("probably@unexistant.addr")) + .await .unwrap(); - t.ctx.set_config(Config::MailPw, Some("123456")).unwrap(); - JobConfigureImap(&t.ctx); + t.ctx + .set_config(Config::MailPw, Some("123456")) + .await + .unwrap(); + assert!(t.ctx.configure().await.is_err()); } - #[test] - fn test_get_offline_autoconfig() { - let context = dummy_context().ctx; + #[async_std::test] + async fn test_get_offline_autoconfig() { + let context = dummy_context().await.ctx; let mut params = LoginParam::new(); params.addr = "someone123@example.org".to_string(); diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs index 498f8e0c8..096b7b218 100644 --- a/src/configure/read_url.rs +++ b/src/configure/read_url.rs @@ -3,17 +3,13 @@ use crate::context::Context; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("URL request error")] - GetError(#[from] reqwest::Error), + GetError(surf::Error), } -pub fn read_url(context: &Context, url: &str) -> Result { +pub async fn read_url(context: &Context, url: &str) -> Result { info!(context, "Requesting URL {}", url); - match reqwest::blocking::Client::new() - .get(url) - .send() - .and_then(|res| res.text()) - { + match surf::get(url).recv_string().await { Ok(res) => Ok(res), Err(err) => { info!(context, "Can\'t read URL {}", url); diff --git a/src/contact.rs b/src/contact.rs index 34e3d051e..0fff01025 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,7 +1,6 @@ //! Contacts module -use std::path::PathBuf; - +use async_std::path::PathBuf; use deltachat_derive::*; use itertools::Itertools; @@ -19,7 +18,6 @@ use crate::message::{MessageState, MsgId}; use crate::mimeparser::AvatarAction; use crate::param::*; use crate::peerstate::*; -use crate::sql; use crate::stock::StockMessage; /// An object representing a single contact in memory. @@ -160,32 +158,39 @@ pub enum VerifiedStatus { } impl Contact { - pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result { - let mut res = context.sql.query_row( - "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param + pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result { + let mut res = context + .sql + .query_row( + "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param FROM contacts c WHERE c.id=?;", - params![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>(3)?.unwrap_or_default() != 0, - origin: row.get(2)?, - param: row.get::<_, String>(5)?.parse().unwrap_or_default(), - }; - Ok(contact) - }, - )?; + 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>(3)?.unwrap_or_default() != 0, + origin: row.get(2)?, + param: row.get::<_, String>(5)?.parse().unwrap_or_default(), + }; + Ok(contact) + }, + ) + .await?; if contact_id == DC_CONTACT_ID_SELF { - res.name = context.stock_str(StockMessage::SelfMsg).to_string(); + res.name = context.stock_str(StockMessage::SelfMsg).await.to_string(); res.addr = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); } else if contact_id == DC_CONTACT_ID_DEVICE { - res.name = context.stock_str(StockMessage::DeviceMessages).to_string(); + res.name = context + .stock_str(StockMessage::DeviceMessages) + .await + .to_string(); res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string(); } Ok(res) @@ -197,20 +202,21 @@ impl Contact { } /// Check if a contact is blocked. - pub fn is_blocked_load(context: &Context, id: u32) -> bool { + pub async fn is_blocked_load(context: &Context, id: u32) -> bool { Self::load_from_db(context, id) + .await .map(|contact| contact.blocked) .unwrap_or_default() } /// Block the given contact. - pub fn block(context: &Context, id: u32) { - set_block_contact(context, id, true); + pub async fn block(context: &Context, id: u32) { + set_block_contact(context, id, true).await; } /// Unblock the given contact. - pub fn unblock(context: &Context, id: u32) { - set_block_contact(context, id, false); + pub async fn unblock(context: &Context, id: u32) { + set_block_contact(context, id, false).await; } /// Add a single contact as a result of an _explicit_ user action. @@ -222,16 +228,20 @@ impl Contact { /// a bunch of addresses. /// /// May result in a `#DC_EVENT_CONTACTS_CHANGED` event. - pub fn create(context: &Context, name: impl AsRef, addr: impl AsRef) -> Result { + pub async fn create( + context: &Context, + name: impl AsRef, + addr: impl AsRef, + ) -> Result { ensure!( !addr.as_ref().is_empty(), "Cannot create contact with empty address" ); let (contact_id, sth_modified) = - Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated)?; - let blocked = Contact::is_blocked_load(context, contact_id); - context.call_cb(Event::ContactsChanged( + Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?; + let blocked = Contact::is_blocked_load(context, contact_id).await; + context.emit_event(Event::ContactsChanged( if sth_modified == Modifier::Created { Some(contact_id) } else { @@ -239,7 +249,7 @@ impl Contact { }, )); if blocked { - Contact::unblock(context, contact_id); + Contact::unblock(context, contact_id).await; } Ok(contact_id) @@ -249,16 +259,17 @@ impl Contact { /// as *noticed*. See also dc_marknoticed_chat() and dc_markseen_msgs() /// /// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`. - pub fn mark_noticed(context: &Context, id: u32) { - if sql::execute( - context, - &context.sql, - "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", - params![MessageState::InNoticed, id as i32, MessageState::InFresh], - ) - .is_ok() + pub async fn mark_noticed(context: &Context, id: u32) { + if context + .sql + .execute( + "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", + paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh], + ) + .await + .is_ok() { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); @@ -270,7 +281,11 @@ impl Contact { /// /// To validate an e-mail address independently of the contact database /// use `dc_may_be_valid_addr()`. - pub fn lookup_id_by_addr(context: &Context, addr: impl AsRef, min_origin: Origin) -> u32 { + pub async fn lookup_id_by_addr( + context: &Context, + addr: impl AsRef, + min_origin: Origin, + ) -> u32 { if addr.as_ref().is_empty() { return 0; } @@ -278,6 +293,7 @@ impl Contact { let addr_normalized = addr_normalize(addr.as_ref()); let addr_self = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); if addr_cmp(addr_normalized, addr_self) { @@ -286,12 +302,12 @@ impl Contact { context.sql.query_get_value( context, "SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;", - params![ + paramsv![ addr_normalized, DC_CONTACT_ID_LAST_SPECIAL as i32, min_origin as u32, ], - ).unwrap_or_default() + ).await.unwrap_or_default() } /// Lookup a contact and create it if it does not exist yet. @@ -319,7 +335,7 @@ impl Contact { /// Depending on the origin, both, "row_name" and "row_authname" are updated from "name". /// /// Returns the contact_id and a `Modifier` value indicating if a modification occured. - pub(crate) fn add_or_lookup( + pub(crate) async fn add_or_lookup( context: &Context, name: impl AsRef, addr: impl AsRef, @@ -333,12 +349,13 @@ impl Contact { ); ensure!(origin != Origin::Unknown, "Missing valid origin"); - let addr = addr_normalize(addr.as_ref()); + let addr = addr_normalize(addr.as_ref()).to_string(); let addr_self = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); - if addr_cmp(addr, addr_self) { + if addr_cmp(&addr, addr_self) { return Ok((DC_CONTACT_ID_SELF, sth_modified)); } @@ -363,7 +380,7 @@ impl Contact { 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;", - params![addr], + paramsv![addr.to_string()], |row| { let row_id = row.get(0)?; let row_name: String = row.get(1)?; @@ -391,7 +408,8 @@ impl Contact { Ok((row_id, row_name, row_addr, row_origin, row_authname)) }, - ) { + ) + .await { row_id = id; if origin as i32 >= row_origin as i32 && addr != row_addr { update_addr = true; @@ -399,45 +417,44 @@ impl Contact { if update_name || update_authname || update_addr || origin > row_origin { let new_name = if update_name { if !name.as_ref().is_empty() { - name.as_ref() + name.as_ref().to_string() } else { - &row_authname + row_authname.clone() } } else { - &row_name + row_name }; - sql::execute( - context, - &context.sql, - "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", - params![ - new_name, - if update_addr { addr } else { &row_addr }, - if origin > row_origin { - origin - } else { - row_origin - }, - if update_authname { - name.as_ref() - } else { - &row_authname - }, - row_id - ], - ) - .ok(); + 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.as_ref().to_string() + } else { + row_authname + }, + row_id + ], + ) + .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. - sql::execute( - context, - &context.sql, + context.sql.execute( "UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);", - params![new_name, Chattype::Single, row_id] - ).ok(); + paramsv![new_name, Chattype::Single, row_id] + ).await.ok(); } sth_modified = Modifier::Modified; } @@ -446,22 +463,26 @@ impl Contact { update_authname = true; } - if sql::execute( - context, - &context.sql, - "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", - params![ - name.as_ref(), - addr, - origin, - if update_authname { name.as_ref() } else { "" } - ], - ) - .is_ok() + if context + .sql + .execute( + "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", + paramsv![ + name.as_ref().to_string(), + addr, + origin, + if update_authname { name.as_ref().to_string() } else { "".to_string() } + ], + ) + .await + .is_ok() { - row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr); + row_id = context + .sql + .get_rowid(context, "contacts", "addr", &addr) + .await?; sth_modified = Modifier::Created; - info!(context, "added contact id={} addr={}", row_id, addr); + info!(context, "added contact id={} addr={}", row_id, &addr); } else { error!(context, "Cannot add contact."); } @@ -487,12 +508,12 @@ impl Contact { /// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`. /// /// Returns the number of modified contacts. - pub fn add_address_book(context: &Context, addr_book: impl AsRef) -> Result { + pub async fn add_address_book(context: &Context, addr_book: impl AsRef) -> Result { let mut modify_cnt = 0; for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() { let name = normalize_name(name); - match Contact::add_or_lookup(context, name, addr, Origin::AddressBook) { + match Contact::add_or_lookup(context, name, addr, Origin::AddressBook).await { Err(err) => { warn!( context, @@ -507,7 +528,7 @@ impl Contact { } } if modify_cnt > 0 { - context.call_cb(Event::ContactsChanged(None)); + context.emit_event(Event::ContactsChanged(None)); } Ok(modify_cnt) @@ -522,13 +543,14 @@ impl Contact { /// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned. /// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned. /// `query` is a string to filter the list. - pub fn get_all( + pub async fn get_all( context: &Context, listflags: u32, query: Option>, ) -> Result> { let self_addr = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); let mut add_self = false; @@ -544,8 +566,10 @@ impl Contact { .map(|s| s.as_ref().to_string()) .unwrap_or_default() ); - context.sql.query_map( - "SELECT c.id FROM contacts c \ + context + .sql + .query_map( + "SELECT c.id FROM contacts c \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE c.addr!=?1 \ AND c.id>?2 \ @@ -554,30 +578,34 @@ impl Contact { AND (c.name LIKE ?4 OR c.addr LIKE ?5) \ AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \ ORDER BY LOWER(c.name||c.addr),c.id;", - params![ - self_addr, - DC_CONTACT_ID_LAST_SPECIAL as i32, - Origin::IncomingReplyTo, - &s3str_like_cmd, - &s3str_like_cmd, - if flag_verified_only { 0 } else { 1 }, - ], - |row| row.get::<_, i32>(0), - |ids| { - for id in ids { - ret.push(id? as u32); - } - Ok(()) - }, - )?; + 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(()) + }, + ) + .await?; - let self_name = context.get_config(Config::Displayname).unwrap_or_default(); + let self_name = context + .get_config(Config::Displayname) + .await + .unwrap_or_default(); let self_name2 = context.stock_str(StockMessage::SelfMsg); if let Some(query) = query { if self_addr.contains(query.as_ref()) || self_name.contains(query.as_ref()) - || self_name2.contains(query.as_ref()) + || self_name2.await.contains(query.as_ref()) { add_self = true; } @@ -589,7 +617,7 @@ impl Contact { context.sql.query_map( "SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(name||addr),id;", - params![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100], + paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100], |row| row.get::<_, i32>(0), |ids| { for id in ids { @@ -597,7 +625,7 @@ impl Contact { } Ok(()) } - )?; + ).await?; } if flag_add_self && add_self { @@ -607,30 +635,32 @@ impl Contact { Ok(ret) } - pub fn get_blocked_cnt(context: &Context) -> usize { + pub async fn get_blocked_cnt(context: &Context) -> usize { context .sql - .query_get_value::<_, isize>( + .query_get_value::( context, "SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0", - params![DC_CONTACT_ID_LAST_SPECIAL as i32], + paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], ) + .await .unwrap_or_default() as usize } /// Get blocked contacts. - pub fn get_all_blocked(context: &Context) -> Vec { + pub async fn get_all_blocked(context: &Context) -> Vec { context .sql .query_map( "SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;", - params![DC_CONTACT_ID_LAST_SPECIAL as i32], + paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], |row| row.get::<_, u32>(0), |ids| { ids.collect::, _>>() .map_err(Into::into) }, ) + .await .unwrap_or_default() } @@ -639,12 +669,12 @@ impl Contact { /// This function returns a string explaining the encryption state /// of the contact and if the connection is encrypted the /// fingerprints of the keys involved. - pub fn get_encrinfo(context: &Context, contact_id: u32) -> Result { + pub async fn get_encrinfo(context: &Context, contact_id: u32) -> Result { let mut ret = String::new(); - if let Ok(contact) = Contact::load_from_db(context, contact_id) { - let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr); - let loginparam = LoginParam::from_database(context, "configured_"); + if let Ok(contact) = Contact::load_from_db(context, contact_id).await { + let peerstate = Peerstate::from_addr(context, &contact.addr).await; + let loginparam = LoginParam::from_database(context, "configured_").await; if peerstate.is_some() && peerstate @@ -653,15 +683,16 @@ impl Contact { .is_some() { let peerstate = peerstate.as_ref().unwrap(); - let p = - context.stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual { + let p = context + .stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual { StockMessage::E2ePreferred } else { StockMessage::E2eAvailable - }); + }) + .await; ret += &p; - let self_key = Key::from(SignedPublicKey::load_self(context)?); - let p = context.stock_str(StockMessage::FingerPrints); + let self_key = Key::from(SignedPublicKey::load_self(context).await?); + let p = context.stock_str(StockMessage::FingerPrints).await; ret += &format!(" {}:", p); let fingerprint_self = self_key.formatted_fingerprint(); @@ -693,9 +724,9 @@ impl Contact { } else if 0 == loginparam.server_flags & DC_LP_IMAP_SOCKET_PLAIN as i32 && 0 == loginparam.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 { - ret += &context.stock_str(StockMessage::EncrTransp); + ret += &context.stock_str(StockMessage::EncrTransp).await; } else { - ret += &context.stock_str(StockMessage::EncrNone); + ret += &context.stock_str(StockMessage::EncrNone).await; } } @@ -706,7 +737,7 @@ impl Contact { /// possible as the contact is in use. In this case, the contact can be blocked. /// /// May result in a `#DC_EVENT_CONTACTS_CHANGED` event. - pub fn delete(context: &Context, contact_id: u32) -> Result<()> { + pub async fn delete(context: &Context, contact_id: u32) -> Result<()> { ensure!( contact_id > DC_CONTACT_ID_LAST_SPECIAL, "Can not delete special contact" @@ -717,8 +748,9 @@ impl Contact { .query_get_value( context, "SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;", - params![contact_id as i32], + paramsv![contact_id as i32], ) + .await .unwrap_or_default(); let count_msgs: i32 = if count_contacts > 0 { @@ -727,22 +759,25 @@ impl Contact { .query_get_value( context, "SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;", - params![contact_id as i32, contact_id as i32], + paramsv![contact_id as i32, contact_id as i32], ) + .await .unwrap_or_default() } else { 0 }; if count_msgs == 0 { - match sql::execute( - context, - &context.sql, - "DELETE FROM contacts WHERE id=?;", - params![contact_id as i32], - ) { + match context + .sql + .execute( + "DELETE FROM contacts WHERE id=?;", + paramsv![contact_id as i32], + ) + .await + { Ok(_) => { - context.call_cb(Event::ContactsChanged(None)); + context.emit_event(Event::ContactsChanged(None)); return Ok(()); } Err(err) => { @@ -764,17 +799,20 @@ impl Contact { /// For contact DC_CONTACT_ID_SELF (1), the function returns sth. /// like "Me" in the selected language and the email address /// defined by dc_set_config(). - pub fn get_by_id(context: &Context, contact_id: u32) -> Result { - Ok(Contact::load_from_db(context, contact_id)?) + pub async fn get_by_id(context: &Context, contact_id: u32) -> Result { + let contact = Contact::load_from_db(context, contact_id).await?; + + Ok(contact) } - pub fn update_param(&mut self, context: &Context) -> Result<()> { - sql::execute( - context, - &context.sql, - "UPDATE contacts SET param=? WHERE id=?", - params![self.param.to_string(), self.id as i32], - )?; + pub async fn update_param(&mut self, context: &Context) -> Result<()> { + context + .sql + .execute( + "UPDATE contacts SET param=? WHERE id=?", + paramsv![self.param.to_string(), self.id as i32], + ) + .await?; Ok(()) } @@ -844,9 +882,9 @@ 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 fn get_profile_image(&self, context: &Context) -> Option { + pub async fn get_profile_image(&self, context: &Context) -> Option { if self.id == DC_CONTACT_ID_SELF { - if let Some(p) = context.get_config(Config::Selfavatar) { + if let Some(p) = context.get_config(Config::Selfavatar).await { return Some(PathBuf::from(p)); } } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { @@ -870,17 +908,17 @@ impl Contact { /// /// The UI may draw a checkbox or something like that beside verified contacts. /// - pub fn is_verified(&self, context: &Context) -> VerifiedStatus { - self.is_verified_ex(context, None) + pub async fn is_verified(&self, context: &Context) -> VerifiedStatus { + self.is_verified_ex(context, None).await } /// Same as `Contact::is_verified` but allows speeding up things /// by adding the peerstate belonging to the contact. /// If you do not have the peerstate available, it is loaded automatically. - pub fn is_verified_ex( + pub async fn is_verified_ex( &self, context: &Context, - peerstate: Option<&Peerstate>, + peerstate: Option<&Peerstate<'_>>, ) -> VerifiedStatus { // We're always sort of secured-verified as we could verify the key on this device any time with the key // on this device @@ -894,7 +932,7 @@ impl Contact { } } - let peerstate = Peerstate::from_addr(context, &context.sql, &self.addr); + let peerstate = Peerstate::from_addr(context, &self.addr).await; if let Some(ps) = peerstate { if ps.verified_key.is_some() { return VerifiedStatus::BidirectVerified; @@ -904,12 +942,16 @@ impl Contact { VerifiedStatus::Unverified } - pub fn addr_equals_contact(context: &Context, addr: impl AsRef, contact_id: u32) -> bool { + pub async fn addr_equals_contact( + context: &Context, + addr: impl AsRef, + contact_id: u32, + ) -> bool { if addr.as_ref().is_empty() { return false; } - if let Ok(contact) = Contact::load_from_db(context, contact_id) { + if let Ok(contact) = Contact::load_from_db(context, contact_id).await { if !contact.addr.is_empty() { let normalized_addr = addr_normalize(addr.as_ref()); if contact.addr == normalized_addr { @@ -921,23 +963,24 @@ impl Contact { false } - pub fn get_real_cnt(context: &Context) -> usize { - if !context.sql.is_open() { + pub async fn get_real_cnt(context: &Context) -> usize { + if !context.sql.is_open().await { return 0; } context .sql - .query_get_value::<_, isize>( + .query_get_value::( context, "SELECT COUNT(*) FROM contacts WHERE id>?;", - params![DC_CONTACT_ID_LAST_SPECIAL as i32], + paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], ) + .await .unwrap_or_default() as usize } - pub fn real_exists_by_id(context: &Context, contact_id: u32) -> bool { - if !context.sql.is_open() || contact_id <= DC_CONTACT_ID_LAST_SPECIAL { + pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool { + if !context.sql.is_open().await || contact_id <= DC_CONTACT_ID_LAST_SPECIAL { return false; } @@ -945,18 +988,20 @@ impl Contact { .sql .exists( "SELECT id FROM contacts WHERE id=?;", - params![contact_id as i32], + paramsv![contact_id as i32], ) + .await .unwrap_or_default() } - pub fn scaleup_origin_by_id(context: &Context, contact_id: u32, origin: Origin) -> bool { + pub async fn scaleup_origin_by_id(context: &Context, contact_id: u32, origin: Origin) -> bool { context .sql .execute( "UPDATE contacts SET origin=? WHERE id=? AND origin &str { norm } -fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) { +async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) { if contact_id <= DC_CONTACT_ID_LAST_SPECIAL { return; } - if let Ok(contact) = Contact::load_from_db(context, contact_id) { + if let Ok(contact) = Contact::load_from_db(context, contact_id).await { if contact.blocked != new_blocking - && sql::execute( - context, - &context.sql, - "UPDATE contacts SET blocked=? WHERE id=?;", - params![new_blocking as i32, contact_id as i32], - ) - .is_ok() + && context + .sql + .execute( + "UPDATE contacts SET blocked=? WHERE id=?;", + paramsv![new_blocking as i32, contact_id as i32], + ) + .await + .is_ok() { // also (un)block all chats with _only_ this contact - we do not delete them to allow a // non-destructive blocking->unblocking. // (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 sql::execute( - context, - &context.sql, + if context.sql.execute( "UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);", - params![new_blocking, 100, contact_id as i32], - ).is_ok() { - Contact::mark_noticed(context, contact_id); - context.call_cb(Event::ContactsChanged(None)); + paramsv![new_blocking, 100, contact_id as i32], + ).await.is_ok() { + Contact::mark_noticed(context, contact_id).await; + context.emit_event(Event::ContactsChanged(None)); } } } } -pub(crate) fn set_profile_image( +pub(crate) async fn set_profile_image( context: &Context, contact_id: u32, profile_image: &AvatarAction, ) -> Result<()> { // the given profile image is expected to be already in the blob directory // as profile images can be set only by receiving messages, this should be always the case, however. - let mut contact = Contact::load_from_db(context, contact_id)?; + let mut contact = Contact::load_from_db(context, contact_id).await?; let changed = match profile_image { AvatarAction::Change(profile_image) => { contact.param.set(Param::ProfileImage, profile_image); @@ -1035,8 +1079,8 @@ pub(crate) fn set_profile_image( } }; if changed { - contact.update_param(context)?; - context.call_cb(Event::ContactsChanged(Some(contact_id))); + contact.update_param(context).await?; + context.emit_event(Event::ContactsChanged(Some(contact_id))); } Ok(()) } @@ -1107,9 +1151,10 @@ fn cat_fingerprint( impl Context { /// determine whether the specified addr maps to the/a self addr - pub fn is_self_addr(&self, addr: &str) -> Result { + pub async fn is_self_addr(&self, addr: &str) -> Result { let self_addr = self .get_config(Config::ConfiguredAddr) + .await .ok_or_else(|| format_err!("Not configured"))?; Ok(addr_cmp(self_addr, addr)) @@ -1194,38 +1239,46 @@ mod tests { ) } - #[test] - fn test_get_contacts() { - let context = dummy_context(); - let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap(); + #[async_std::test] + async fn test_get_contacts() { + let context = dummy_context().await; + let contacts = Contact::get_all(&context.ctx, 0, Some("some2")) + .await + .unwrap(); assert_eq!(contacts.len(), 0); - let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap(); + let id = Contact::create(&context.ctx, "bob", "bob@mail.de") + .await + .unwrap(); assert_ne!(id, 0); - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap(); + let contacts = Contact::get_all(&context.ctx, 0, Some("bob")) + .await + .unwrap(); assert_eq!(contacts.len(), 1); - let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap(); + let contacts = Contact::get_all(&context.ctx, 0, Some("alice")) + .await + .unwrap(); assert_eq!(contacts.len(), 0); } - #[test] - fn test_is_self_addr() -> Result<()> { - let t = test_context(None); - assert!(t.ctx.is_self_addr("me@me.org").is_err()); + #[async_std::test] + async fn test_is_self_addr() -> Result<()> { + let t = test_context().await; + assert!(t.ctx.is_self_addr("me@me.org").await.is_err()); - let addr = configure_alice_keypair(&t.ctx); - assert_eq!(t.ctx.is_self_addr("me@me.org")?, false); - assert_eq!(t.ctx.is_self_addr(&addr)?, true); + let addr = configure_alice_keypair(&t.ctx).await; + assert_eq!(t.ctx.is_self_addr("me@me.org").await?, false); + assert_eq!(t.ctx.is_self_addr(&addr).await?, true); Ok(()) } - #[test] - fn test_add_or_lookup() { + #[async_std::test] + async fn test_add_or_lookup() { // add some contacts, this also tests add_address_book() - let t = dummy_context(); + let t = dummy_context().await; let book = concat!( " Name one \n one@eins.org \n", "Name two\ntwo@deux.net\n", @@ -1233,15 +1286,16 @@ mod tests { "\nthree@drei.sam\n", "Name two\ntwo@deux.net\n" // should not be added again ); - assert_eq!(Contact::add_address_book(&t.ctx, book).unwrap(), 3); + assert_eq!(Contact::add_address_book(&t.ctx, book).await.unwrap(), 3); // check first added contact, this does not modify because of lower origin let (contact_id, sth_modified) = Contact::add_or_lookup(&t.ctx, "bla foo", "one@eins.org", Origin::IncomingUnknownTo) + .await .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); assert_eq!(sth_modified, Modifier::None); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_id(), contact_id); assert_eq!(contact.get_name(), "Name one"); assert_eq!(contact.get_display_name(), "Name one"); @@ -1255,10 +1309,11 @@ mod tests { " one@eins.org ", Origin::ManuallyCreated, ) + .await .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_name(), "Real one"); assert_eq!(contact.get_addr(), "one@eins.org"); assert!(!contact.is_blocked()); @@ -1266,10 +1321,11 @@ mod tests { // check third added contact (contact without name) let (contact_id, sth_modified) = Contact::add_or_lookup(&t.ctx, "", "three@drei.sam", Origin::IncomingUnknownTo) + .await .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); assert_eq!(sth_modified, Modifier::None); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "three@drei.sam"); assert_eq!(contact.get_addr(), "three@drei.sam"); @@ -1282,10 +1338,11 @@ mod tests { "three@drei.sam", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)"); assert!(!contact.is_blocked()); @@ -1296,25 +1353,31 @@ mod tests { "three@drei.sam", Origin::ManuallyCreated, ) + .await .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "m. serious"); assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)"); assert!(!contact.is_blocked()); // check SELF - let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF).unwrap(); + let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF) + .await + .unwrap(); assert_eq!(DC_CONTACT_ID_SELF, 1); - assert_eq!(contact.get_name(), t.ctx.stock_str(StockMessage::SelfMsg)); + assert_eq!( + contact.get_name(), + t.ctx.stock_str(StockMessage::SelfMsg).await + ); assert_eq!(contact.get_addr(), ""); // we're not configured assert!(!contact.is_blocked()); } - #[test] - fn test_remote_authnames() { - let t = dummy_context(); + #[async_std::test] + async fn test_remote_authnames() { + let t = dummy_context().await; // incoming mail `From: bob1 ` - this should init authname and name let (contact_id, sth_modified) = Contact::add_or_lookup( @@ -1323,10 +1386,11 @@ mod tests { "bob@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); assert_eq!(sth_modified, Modifier::Created); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob1"); assert_eq!(contact.get_name(), "bob1"); assert_eq!(contact.get_display_name(), "bob1"); @@ -1338,18 +1402,21 @@ mod tests { "bob@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob2"); assert_eq!(contact.get_name(), "bob2"); assert_eq!(contact.get_display_name(), "bob2"); // manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above - let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap(); + let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org") + .await + .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob2"); assert_eq!(contact.get_name(), "bob3"); assert_eq!(contact.get_display_name(), "bob3"); @@ -1361,23 +1428,26 @@ mod tests { "bob@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "bob4"); assert_eq!(contact.get_name(), "bob3"); assert_eq!(contact.get_display_name(), "bob3"); } - #[test] - fn test_remote_authnames_create_empty() { - let t = dummy_context(); + #[async_std::test] + async fn test_remote_authnames_create_empty() { + let t = dummy_context().await; // manually create "claire@example.org" without a given name - let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap(); + let contact_id = Contact::create(&t.ctx, "", "claire@example.org") + .await + .unwrap(); assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_name(), ""); assert_eq!(contact.get_display_name(), "claire@example.org"); @@ -1389,10 +1459,11 @@ mod tests { "claire@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "claire1"); assert_eq!(contact.get_name(), "claire1"); assert_eq!(contact.get_display_name(), "claire1"); @@ -1404,22 +1475,25 @@ mod tests { "claire@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "claire2"); assert_eq!(contact.get_name(), "claire2"); assert_eq!(contact.get_display_name(), "claire2"); } - #[test] - fn test_remote_authnames_edit_empty() { - let t = dummy_context(); + #[async_std::test] + async fn test_remote_authnames_edit_empty() { + let t = dummy_context().await; // manually create "dave@example.org" - let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap(); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org") + .await + .unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_name(), "dave1"); assert_eq!(contact.get_display_name(), "dave1"); @@ -1431,15 +1505,18 @@ mod tests { "dave@example.org", Origin::IncomingUnknownFrom, ) + .await .unwrap(); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), "dave1"); assert_eq!(contact.get_display_name(), "dave1"); // manually clear the name - Contact::create(&t.ctx, "", "dave@example.org").unwrap(); - let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap(); + Contact::create(&t.ctx, "", "dave@example.org") + .await + .unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), "dave2"); assert_eq!(contact.get_display_name(), "dave2"); diff --git a/src/context.rs b/src/context.rs index 5bf4647d0..cb6370b51 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,8 +2,10 @@ use std::collections::{BTreeMap, HashMap}; use std::ffi::OsString; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Condvar, Mutex, RwLock}; +use std::ops::Deref; + +use async_std::path::{Path, PathBuf}; +use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender}; use crate::chat::*; use crate::config::Config; @@ -11,61 +13,58 @@ use crate::constants::*; use crate::contact::*; use crate::dc_tools::duration_to_str; use crate::error::*; -use crate::events::Event; -use crate::imap::*; -use crate::job::*; -use crate::job_thread::JobThread; +use crate::events::{Event, EventEmitter, Events}; +use crate::job::{self, Action}; use crate::key::{DcKey, Key, SignedPublicKey}; use crate::login_param::LoginParam; use crate::lot::Lot; use crate::message::{self, Message, MessengerMessage, MsgId}; use crate::param::Params; -use crate::smtp::Smtp; +use crate::scheduler::Scheduler; use crate::sql::Sql; use std::time::SystemTime; -/// Callback function type for [Context] -/// -/// # Parameters -/// -/// * `context` - The context object as returned by [Context::new]. -/// * `event` - One of the [Event] items. -/// * `data1` - Depends on the event parameter, see [Event]. -/// * `data2` - Depends on the event parameter, see [Event]. -pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync; - -#[derive(DebugStub)] +#[derive(Clone, Debug)] pub struct Context { + pub(crate) inner: Arc, +} + +impl Deref for Context { + type Target = InnerContext; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug)] +pub struct InnerContext { /// Database file path - dbfile: PathBuf, + pub(crate) dbfile: PathBuf, /// Blob directory path - blobdir: PathBuf, - pub sql: Sql, - pub perform_inbox_jobs_needed: Arc>, - pub probe_imap_network: Arc>, - pub inbox_thread: Arc>, - pub sentbox_thread: Arc>, - pub mvbox_thread: Arc>, - pub smtp: Arc>, - pub smtp_state: Arc<(Mutex, Condvar)>, - pub oauth2_critical: Arc>, - #[debug_stub = "Callback"] - cb: Box, - pub os_name: Option, - pub cmdline_sel_chat_id: Arc>, - pub(crate) bob: Arc>, - pub last_smeared_timestamp: RwLock, - pub running_state: Arc>, + pub(crate) blobdir: PathBuf, + pub(crate) sql: Sql, + pub(crate) os_name: Option, + pub(crate) bob: RwLock, + pub(crate) last_smeared_timestamp: RwLock, + pub(crate) running_state: RwLock, /// Mutex to avoid generating the key for the user more than once. - pub generating_key_mutex: Mutex<()>, - pub translated_stockstrings: RwLock>, + pub(crate) generating_key_mutex: Mutex<()>, + /// Mutex to enforce only a single running oauth2 is running. + pub(crate) oauth2_mutex: Mutex<()>, + pub(crate) translated_stockstrings: RwLock>, + pub(crate) events: Events, + + pub(crate) scheduler: RwLock, + creation_time: SystemTime, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub struct RunningState { pub ongoing_running: bool, shall_stop_ongoing: bool, + cancel_sender: Option>, } /// Return some info about deltachat-core @@ -85,73 +84,95 @@ pub fn get_info() -> BTreeMap<&'static str, String> { impl Context { /// Creates new context. - pub fn new(cb: Box, os_name: String, dbfile: PathBuf) -> Result { - pretty_env_logger::try_init_timed().ok(); + pub async fn new(os_name: String, dbfile: PathBuf) -> Result { + // pretty_env_logger::try_init_timed().ok(); let mut blob_fname = OsString::new(); blob_fname.push(dbfile.file_name().unwrap_or_default()); blob_fname.push("-blobs"); let blobdir = dbfile.with_file_name(blob_fname); - if !blobdir.exists() { - std::fs::create_dir_all(&blobdir)?; + if !blobdir.exists().await { + async_std::fs::create_dir_all(&blobdir).await?; } - Context::with_blobdir(cb, os_name, dbfile, blobdir) + Context::with_blobdir(os_name, dbfile, blobdir).await } - pub fn with_blobdir( - cb: Box, + pub async fn with_blobdir( os_name: String, dbfile: PathBuf, blobdir: PathBuf, ) -> Result { ensure!( - blobdir.is_dir(), + blobdir.is_dir().await, "Blobdir does not exist: {}", blobdir.display() ); - let ctx = Context { + + let inner = InnerContext { blobdir, dbfile, - cb, os_name: Some(os_name), - running_state: Arc::new(RwLock::new(Default::default())), + running_state: RwLock::new(Default::default()), sql: Sql::new(), - smtp: Arc::new(Mutex::new(Smtp::new())), - smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())), - oauth2_critical: Arc::new(Mutex::new(())), - bob: Arc::new(RwLock::new(Default::default())), + bob: RwLock::new(Default::default()), last_smeared_timestamp: RwLock::new(0), - cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))), - inbox_thread: Arc::new(RwLock::new(JobThread::new( - "INBOX", - "configured_inbox_folder", - Imap::new(), - ))), - sentbox_thread: Arc::new(RwLock::new(JobThread::new( - "SENTBOX", - "configured_sentbox_folder", - Imap::new(), - ))), - mvbox_thread: Arc::new(RwLock::new(JobThread::new( - "MVBOX", - "configured_mvbox_folder", - Imap::new(), - ))), - probe_imap_network: Arc::new(RwLock::new(false)), - perform_inbox_jobs_needed: Arc::new(RwLock::new(false)), generating_key_mutex: Mutex::new(()), + oauth2_mutex: Mutex::new(()), translated_stockstrings: RwLock::new(HashMap::new()), + events: Events::default(), + scheduler: RwLock::new(Scheduler::Stopped), creation_time: std::time::SystemTime::now(), }; + let ctx = Context { + inner: Arc::new(inner), + }; ensure!( - ctx.sql.open(&ctx, &ctx.dbfile, false), + ctx.sql.open(&ctx, &ctx.dbfile, false).await, "Failed opening sqlite database" ); Ok(ctx) } + /// Starts the IO scheduler. + pub async fn start_io(&self) { + info!(self, "starting IO"); + if self.is_io_running().await { + info!(self, "IO is already running"); + return; + } + + { + let l = &mut *self.inner.scheduler.write().await; + l.start(self.clone()).await; + } + } + + /// Returns if the IO scheduler is running. + pub async fn is_io_running(&self) -> bool { + self.inner.is_io_running().await + } + + /// Stops the IO scheduler. + pub async fn stop_io(&self) { + info!(self, "stopping IO"); + if !self.is_io_running().await { + info!(self, "IO is not running"); + return; + } + + self.inner.stop_io().await; + } + + /// Returns a reference to the underlying SQL instance. + /// + /// Warning: this is only here for testing, not part of the public API. + #[cfg(feature = "internals")] + pub fn sql(&self) -> &Sql { + &self.inner.sql + } + /// Returns database file path. pub fn get_dbfile(&self) -> &Path { self.dbfile.as_path() @@ -162,49 +183,57 @@ impl Context { self.blobdir.as_path() } - pub fn call_cb(&self, event: Event) { - (*self.cb)(self, event); + /// Emits a single event. + pub fn emit_event(&self, event: Event) { + self.events.emit(event); } - /******************************************************************************* - * Ongoing process allocation/free/check - ******************************************************************************/ + /// Get the next queued event. + pub fn get_event_emitter(&self) -> EventEmitter { + self.events.get_emitter() + } - pub fn alloc_ongoing(&self) -> bool { - if self.has_ongoing() { - warn!(self, "There is already another ongoing process running.",); + // Ongoing process allocation/free/check - false - } else { - let s_a = self.running_state.clone(); - let mut s = s_a.write().unwrap(); - - s.ongoing_running = true; - s.shall_stop_ongoing = false; - - true + pub async fn alloc_ongoing(&self) -> Result> { + if self.has_ongoing().await { + bail!("There is already another ongoing process running."); } + + let s_a = &self.running_state; + let mut s = s_a.write().await; + + s.ongoing_running = true; + s.shall_stop_ongoing = false; + let (sender, receiver) = channel(1); + s.cancel_sender = Some(sender); + + Ok(receiver) } - pub fn free_ongoing(&self) { - let s_a = self.running_state.clone(); - let mut s = s_a.write().unwrap(); + pub async fn free_ongoing(&self) { + let s_a = &self.running_state; + let mut s = s_a.write().await; s.ongoing_running = false; s.shall_stop_ongoing = true; + s.cancel_sender.take(); } - pub fn has_ongoing(&self) -> bool { - let s_a = self.running_state.clone(); - let s = s_a.read().unwrap(); + pub async fn has_ongoing(&self) -> bool { + let s_a = &self.running_state; + let s = s_a.read().await; s.ongoing_running || !s.shall_stop_ongoing } /// Signal an ongoing process to stop. - pub fn stop_ongoing(&self) { - let s_a = self.running_state.clone(); - let mut s = s_a.write().unwrap(); + pub async fn stop_ongoing(&self) { + let s_a = &self.running_state; + let mut s = s_a.write().await; + if let Some(cancel) = s.cancel_sender.take() { + cancel.send(()).await; + } if s.ongoing_running && !s.shall_stop_ongoing { info!(self, "Signaling the ongoing process to stop ASAP.",); @@ -214,71 +243,71 @@ impl Context { }; } - pub fn shall_stop_ongoing(&self) -> bool { - self.running_state - .clone() - .read() - .unwrap() - .shall_stop_ongoing + pub async fn shall_stop_ongoing(&self) -> bool { + self.running_state.read().await.shall_stop_ongoing } /******************************************************************************* * UI chat/message related API ******************************************************************************/ - pub fn get_info(&self) -> BTreeMap<&'static str, String> { + pub async fn get_info(&self) -> BTreeMap<&'static str, String> { let unset = "0"; - let l = LoginParam::from_database(self, ""); - let l2 = LoginParam::from_database(self, "configured_"); - let displayname = self.get_config(Config::Displayname); - let chats = get_chat_cnt(self) as usize; - let real_msgs = message::get_real_msg_cnt(self) as usize; - let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize; - let contacts = Contact::get_real_cnt(self) as usize; - let is_configured = self.get_config_int(Config::Configured); + let 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 dbversion = self .sql .get_raw_config_int(self, "dbversion") + .await .unwrap_or_default(); let journal_mode = self .sql - .query_get_value(self, "PRAGMA journal_mode;", rusqlite::NO_PARAMS) + .query_get_value(self, "PRAGMA journal_mode;", paramsv![]) + .await .unwrap_or_else(|| "unknown".to_string()); - let e2ee_enabled = self.get_config_int(Config::E2eeEnabled); - let mdns_enabled = self.get_config_int(Config::MdnsEnabled); - let bcc_self = self.get_config_int(Config::BccSelf); + let 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 = - self.sql - .query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS); + let prv_key_cnt: Option = self + .sql + .query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![]) + .await; - let pub_key_cnt: Option = self.sql.query_get_value( - self, - "SELECT COUNT(*) FROM acpeerstates;", - rusqlite::NO_PARAMS, - ); - - let fingerprint_str = match SignedPublicKey::load_self(self) { + let pub_key_cnt: Option = self + .sql + .query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![]) + .await; + let fingerprint_str = match SignedPublicKey::load_self(self).await { Ok(key) => Key::from(key).fingerprint(), Err(err) => format!("", err), }; - let inbox_watch = self.get_config_int(Config::InboxWatch); - let sentbox_watch = self.get_config_int(Config::SentboxWatch); - let mvbox_watch = self.get_config_int(Config::MvboxWatch); - let mvbox_move = self.get_config_int(Config::MvboxMove); + let 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 folders_configured = self .sql .get_raw_config_int(self, "folders_configured") + .await .unwrap_or_default(); let configured_sentbox_folder = self .sql .get_raw_config(self, "configured_sentbox_folder") + .await .unwrap_or_else(|| "".to_string()); let configured_mvbox_folder = self .sql .get_raw_config(self, "configured_mvbox_folder") + .await .unwrap_or_else(|| "".to_string()); let mut res = get_info(); @@ -294,6 +323,7 @@ impl Context { res.insert( "selfavatar", self.get_config(Config::Selfavatar) + .await .unwrap_or_else(|| "".to_string()), ); res.insert("is_configured", is_configured.to_string()); @@ -325,8 +355,8 @@ impl Context { res } - pub fn get_fresh_msgs(&self) -> Vec { - let show_deaddrop = 0; + pub async fn get_fresh_msgs(&self) -> Vec { + let show_deaddrop: i32 = 0; self.sql .query_map( concat!( @@ -343,7 +373,7 @@ impl Context { " AND (c.blocked=0 OR c.blocked=?)", " ORDER BY m.timestamp DESC,m.id DESC;" ), - &[10, 9, if 0 != show_deaddrop { 2 } else { 0 }], + paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }], |row| row.get::<_, MsgId>(0), |rows| { let mut ret = Vec::new(); @@ -353,11 +383,12 @@ impl Context { Ok(ret) }, ) + .await .unwrap_or_default() } #[allow(non_snake_case)] - pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef) -> Vec { + pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef) -> Vec { let real_query = query.as_ref().trim(); if real_query.is_empty() { return Vec::new(); @@ -397,7 +428,7 @@ impl Context { self.sql .query_map( query, - params![chat_id, &strLikeInText, &strLikeBeg], + paramsv![chat_id, strLikeInText, strLikeBeg], |row| row.get::<_, MsgId>("id"), |rows| { let mut ret = Vec::new(); @@ -407,6 +438,7 @@ impl Context { Ok(ret) }, ) + .await .unwrap_or_default() } @@ -414,8 +446,11 @@ impl Context { folder_name.as_ref() == "INBOX" } - pub fn is_sentbox(&self, folder_name: impl AsRef) -> bool { - let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder"); + pub async fn is_sentbox(&self, folder_name: impl AsRef) -> bool { + let sentbox_name = self + .sql + .get_raw_config(self, "configured_sentbox_folder") + .await; if let Some(name) = sentbox_name { name == folder_name.as_ref() } else { @@ -423,8 +458,11 @@ impl Context { } } - pub fn is_mvbox(&self, folder_name: impl AsRef) -> bool { - let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder"); + pub async fn is_mvbox(&self, folder_name: impl AsRef) -> bool { + let mvbox_name = self + .sql + .get_raw_config(self, "configured_mvbox_folder") + .await; if let Some(name) = mvbox_name { name == folder_name.as_ref() @@ -433,15 +471,15 @@ impl Context { } } - pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) { - if !self.get_config_bool(Config::MvboxMove) { + pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) { + if !self.get_config_bool(Config::MvboxMove).await { return; } - if self.is_mvbox(folder) { + if self.is_mvbox(folder).await { return; } - if let Ok(msg) = Message::load_from_db(self, msg_id) { + if let Ok(msg) = Message::load_from_db(self, msg_id).await { if msg.is_setupmessage() { // do not move setup messages; // there may be a non-delta device that wants to handle it @@ -451,30 +489,32 @@ impl Context { match msg.is_dc_message { MessengerMessage::No => {} MessengerMessage::Yes | MessengerMessage::Reply => { - job_add( + job::add( self, - Action::MoveMsg, - msg.id.to_u32() as i32, - Params::new(), - 0, - ); + job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0), + ) + .await; } } } } } -impl Drop for Context { - fn drop(&mut self) { - info!(self, "disconnecting inbox-thread",); - self.inbox_thread.read().unwrap().imap.disconnect(self); - info!(self, "disconnecting sentbox-thread",); - self.sentbox_thread.read().unwrap().imap.disconnect(self); - info!(self, "disconnecting mvbox-thread",); - self.mvbox_thread.read().unwrap().imap.disconnect(self); - info!(self, "disconnecting SMTP"); - self.smtp.clone().lock().unwrap().disconnect(); - self.sql.close(self); +impl InnerContext { + async fn is_io_running(&self) -> bool { + self.scheduler.read().await.is_running() + } + + async fn stop_io(&self) { + assert!(self.is_io_running().await, "context is already stopped"); + let token = { + let lock = &*self.scheduler.read().await; + lock.pre_stop().await + }; + { + let lock = &mut *self.scheduler.write().await; + lock.stop(token).await; + } } } @@ -483,6 +523,7 @@ impl Default for RunningState { RunningState { ongoing_running: false, shall_stop_ongoing: true, + cancel_sender: None, } } } @@ -494,28 +535,6 @@ pub(crate) struct BobStatus { pub qr_scan: Option, } -#[derive(Debug, PartialEq)] -pub(crate) enum PerformJobsNeeded { - Not, - AtOnce, - AvoidDos, -} - -impl Default for PerformJobsNeeded { - fn default() -> Self { - Self::Not - } -} - -#[derive(Default, Debug)] -pub struct SmtpState { - pub idle: bool, - pub suspended: bool, - pub doing_jobs: bool, - pub(crate) perform_jobs_needed: PerformJobsNeeded, - pub probe_network: bool, -} - pub fn get_version_str() -> &'static str { &DC_VERSION_STR } @@ -526,81 +545,81 @@ mod tests { use crate::test_utils::*; - #[test] - fn test_wrong_db() { + #[async_std::test] + async fn test_wrong_db() { let tmp = tempfile::tempdir().unwrap(); let dbfile = tmp.path().join("db.sqlite"); std::fs::write(&dbfile, b"123").unwrap(); - let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile); + let res = Context::new("FakeOs".into(), dbfile.into()).await; assert!(res.is_err()); } - #[test] - fn test_get_fresh_msgs() { - let t = dummy_context(); - let fresh = t.ctx.get_fresh_msgs(); + #[async_std::test] + async fn test_get_fresh_msgs() { + let t = dummy_context().await; + let fresh = t.ctx.get_fresh_msgs().await; assert!(fresh.is_empty()) } - #[test] - fn test_blobdir_exists() { + #[async_std::test] + async fn test_blobdir_exists() { let tmp = tempfile::tempdir().unwrap(); let dbfile = tmp.path().join("db.sqlite"); - Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap(); + Context::new("FakeOS".into(), dbfile.into()).await.unwrap(); let blobdir = tmp.path().join("db.sqlite-blobs"); assert!(blobdir.is_dir()); } - #[test] - fn test_wrong_blogdir() { + #[async_std::test] + async fn test_wrong_blogdir() { let tmp = tempfile::tempdir().unwrap(); let dbfile = tmp.path().join("db.sqlite"); let blobdir = tmp.path().join("db.sqlite-blobs"); std::fs::write(&blobdir, b"123").unwrap(); - let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile); + let res = Context::new("FakeOS".into(), dbfile.into()).await; assert!(res.is_err()); } - #[test] - fn test_sqlite_parent_not_exists() { + #[async_std::test] + async fn test_sqlite_parent_not_exists() { let tmp = tempfile::tempdir().unwrap(); let subdir = tmp.path().join("subdir"); let dbfile = subdir.join("db.sqlite"); let dbfile2 = dbfile.clone(); - Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap(); + Context::new("FakeOS".into(), dbfile.into()).await.unwrap(); assert!(subdir.is_dir()); assert!(dbfile2.is_file()); } - #[test] - fn test_with_empty_blobdir() { + #[async_std::test] + async fn test_with_empty_blobdir() { let tmp = tempfile::tempdir().unwrap(); let dbfile = tmp.path().join("db.sqlite"); let blobdir = PathBuf::new(); - let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir); + let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await; assert!(res.is_err()); } - #[test] - fn test_with_blobdir_not_exists() { + #[async_std::test] + async fn test_with_blobdir_not_exists() { let tmp = tempfile::tempdir().unwrap(); let dbfile = tmp.path().join("db.sqlite"); let blobdir = tmp.path().join("blobs"); - let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir); + let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await; assert!(res.is_err()); } - #[test] - fn no_crashes_on_context_deref() { - let t = dummy_context(); + #[async_std::test] + async fn no_crashes_on_context_deref() { + let t = dummy_context().await; std::mem::drop(t.ctx); } - #[test] - fn test_get_info() { - let t = dummy_context(); + #[async_std::test] + async fn test_get_info() { + let t = dummy_context().await; - let info = t.ctx.get_info(); + let info = t.ctx.get_info().await; assert!(info.get("database_dir").is_some()); } diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index cdde8a2de..eb303cadd 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -1,7 +1,6 @@ use itertools::join; -use sha2::{Digest, Sha256}; - use num_traits::FromPrimitive; +use sha2::{Digest, Sha256}; use mailparse::SingleInfo; @@ -14,13 +13,12 @@ use crate::dc_tools::*; use crate::error::{bail, ensure, Result}; use crate::events::Event; use crate::headerdef::HeaderDef; -use crate::job::*; +use crate::job::{self, Action}; use crate::message::{self, MessageState, MessengerMessage, MsgId}; use crate::mimeparser::*; use crate::param::*; use crate::peerstate::*; use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device}; -use crate::sql; use crate::stock::StockMessage; use crate::{contact, location}; @@ -38,7 +36,7 @@ enum CreateEvent { /// Returns an error on recoverable errors, e.g. database errors. In this case, /// message parsing should be retried later. If message itself is wrong, logs /// the error and returns success. -pub fn dc_receive_imf( +pub async fn dc_receive_imf( context: &Context, imf_raw: &[u8], server_folder: impl AsRef, @@ -61,7 +59,7 @@ pub fn dc_receive_imf( println!("{}", String::from_utf8_lossy(imf_raw)); } - let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw) { + let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw).await { Err(err) => { warn!(context, "dc_receive_imf: can't parse MIME: {}", err); return Ok(()); @@ -96,7 +94,7 @@ pub fn dc_receive_imf( CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id }, CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id }, }; - context.call_cb(event); + context.emit_event(event); } } }; @@ -112,23 +110,26 @@ pub fn dc_receive_imf( // we do not check Return-Path any more as this is unreliable, see // https://github.com/deltachat/deltachat-core/issues/150) let (from_id, from_id_blocked, incoming_origin) = - from_field_to_contact_id(context, &mime_parser.from)?; + from_field_to_contact_id(context, &mime_parser.from).await?; let incoming = from_id != DC_CONTACT_ID_SELF; let mut to_ids = ContactIds::new(); - to_ids.extend(&dc_add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - )?); + to_ids.extend( + &dc_add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?, + ); // Add parts @@ -167,7 +168,9 @@ pub fn dc_receive_imf( &mut insert_msg_id, &mut created_db_entries, &mut create_event_to_send, - ) { + ) + .await + { cleanup(context, &create_event_to_send, created_db_entries); bail!("add_parts error: {:?}", err); } @@ -187,13 +190,14 @@ pub fn dc_receive_imf( from_id, insert_msg_id, hidden, - ); + ) + .await; } if let Some(avatar_action) = &mime_parser.user_avatar { - match contact::set_profile_image(&context, from_id, avatar_action) { + match contact::set_profile_image(&context, from_id, avatar_action).await { Ok(()) => { - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); } Err(err) => { warn!(context, "reveive_imf cannot update profile image: {}", err); @@ -202,22 +206,27 @@ pub fn dc_receive_imf( } // Get user-configured server deletion - let delete_server_after = context.get_config_delete_server_after(); + let delete_server_after = context.get_config_delete_server_after().await; if !created_db_entries.is_empty() { if needs_delete_job || delete_server_after == Some(0) { for db_entry in &created_db_entries { - job_add( + job::add( context, - Action::DeleteMsgOnImap, - db_entry.1.to_u32() as i32, - Params::new(), - 0, - ); + job::Job::new( + Action::DeleteMsgOnImap, + db_entry.1.to_u32(), + Params::new(), + 0, + ), + ) + .await; } } else { // Move message if we don't delete it immediately. - context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id); + context + .do_heuristics_moves(server_folder.as_ref(), insert_msg_id) + .await; } } @@ -228,7 +237,9 @@ pub fn dc_receive_imf( cleanup(context, &create_event_to_send, created_db_entries); - mime_parser.handle_reports(context, from_id, sent_timestamp); + mime_parser + .handle_reports(context, from_id, sent_timestamp) + .await; Ok(()) } @@ -236,7 +247,7 @@ pub fn dc_receive_imf( /// Converts "From" field to contact id. /// /// Also returns whether it is blocked or not and its origin. -pub fn from_field_to_contact_id( +pub async fn from_field_to_contact_id( context: &Context, from_address_list: &[SingleInfo], ) -> Result<(u32, bool, Origin)> { @@ -244,7 +255,8 @@ pub fn from_field_to_contact_id( context, from_address_list, Origin::IncomingUnknownFrom, - )?; + ) + .await?; if from_ids.contains(&DC_CONTACT_ID_SELF) { Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc)) @@ -259,7 +271,7 @@ pub fn from_field_to_contact_id( let mut from_id_blocked = false; let mut incoming_origin = Origin::Unknown; - if let Ok(contact) = Contact::load_from_db(context, from_id) { + if let Ok(contact) = Contact::load_from_db(context, from_id).await { from_id_blocked = contact.blocked; incoming_origin = contact.origin; } @@ -278,7 +290,7 @@ pub fn from_field_to_contact_id( } #[allow(clippy::too_many_arguments, clippy::cognitive_complexity)] -fn add_parts( +async fn add_parts( context: &Context, mut mime_parser: &mut MimeMessage, imf_raw: &[u8], @@ -311,10 +323,11 @@ fn add_parts( // (if the mail was moved around) and finish. (we may get a mail twice eg. if it is // moved between folders. make sure, this check is done eg. before securejoin-processing) */ if let Some((old_server_folder, old_server_uid, _)) = - message::rfc724_mid_exists(context, &rfc724_mid)? + message::rfc724_mid_exists(context, &rfc724_mid).await? { if old_server_folder != server_folder.as_ref() || old_server_uid != server_uid { - message::update_server_uid(context, &rfc724_mid, server_folder.as_ref(), server_uid); + message::update_server_uid(context, &rfc724_mid, server_folder.as_ref(), server_uid) + .await; } warn!(context, "Message already in DB"); @@ -323,7 +336,7 @@ fn add_parts( let mut msgrmsg = if mime_parser.has_chat_version() { MessengerMessage::Yes - } else if is_reply_to_messenger_message(context, mime_parser) { + } else if is_reply_to_messenger_message(context, mime_parser).await { MessengerMessage::Reply } else { MessengerMessage::No @@ -331,7 +344,7 @@ fn add_parts( // incoming non-chat messages may be discarded let mut allow_creation = true; let show_emails = - ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default(); + ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await).unwrap_or_default(); if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && msgrmsg == MessengerMessage::No { @@ -365,7 +378,7 @@ fn add_parts( msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting *chat_id = ChatId::new(0); allow_creation = true; - match handle_securejoin_handshake(context, mime_parser, from_id) { + match handle_securejoin_handshake(context, mime_parser, from_id).await { Ok(securejoin::HandshakeMessage::Done) => { *hidden = true; *needs_delete_job = true; @@ -380,8 +393,9 @@ fn add_parts( } Err(err) => { *hidden = true; - context.bob.write().unwrap().status = 0; // secure-join failed - context.stop_ongoing(); + + context.bob.write().await.status = 0; // secure-join failed + context.stop_ongoing().await; warn!(context, "Error in Secure-Join message handling: {}", err); return Ok(()); } @@ -389,7 +403,9 @@ fn add_parts( } let (test_normal_chat_id, test_normal_chat_id_blocked) = - chat::lookup_by_contact_id(context, from_id).unwrap_or_default(); + chat::lookup_by_contact_id(context, from_id) + .await + .unwrap_or_default(); // get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list, // it might also be blocked and displayed in the deaddrop as a result @@ -415,14 +431,15 @@ fn add_parts( create_blocked, from_id, to_ids, - )?; + ) + .await?; *chat_id = new_chat_id; chat_id_blocked = new_chat_id_blocked; if !chat_id.is_unset() && chat_id_blocked != Blocked::Not && create_blocked == Blocked::Not { - new_chat_id.unblock(context); + new_chat_id.unblock(context).await; chat_id_blocked = Blocked::Not; } } @@ -449,18 +466,19 @@ fn add_parts( } else if allow_creation { let (id, bl) = chat::create_or_lookup_by_contact_id(context, from_id, create_blocked) + .await .unwrap_or_default(); *chat_id = id; chat_id_blocked = bl; } if !chat_id.is_unset() && Blocked::Not != chat_id_blocked { if Blocked::Not == create_blocked { - chat_id.unblock(context); + chat_id.unblock(context).await; chat_id_blocked = Blocked::Not; - } else if is_reply_to_known_message(context, mime_parser) { + } else if is_reply_to_known_message(context, mime_parser).await { // we do not want any chat to be created implicitly. Because of the origin-scale-up, // the contact requests will pop up and this should be just fine. - Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo); + Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo).await; info!( context, "Message is a reply to a known message, mark sender as known.", @@ -500,7 +518,7 @@ fn add_parts( msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting *chat_id = ChatId::new(0); allow_creation = true; - match observe_securejoin_on_other_device(context, mime_parser, to_id) { + match observe_securejoin_on_other_device(context, mime_parser, to_id).await { Ok(securejoin::HandshakeMessage::Done) | Ok(securejoin::HandshakeMessage::Ignore) => { *hidden = true; @@ -525,24 +543,26 @@ fn add_parts( Blocked::Not, from_id, to_ids, - )?; + ) + .await?; *chat_id = new_chat_id; chat_id_blocked = new_chat_id_blocked; // automatically unblock chat when the user sends a message if !chat_id.is_unset() && chat_id_blocked != Blocked::Not { - new_chat_id.unblock(context); + new_chat_id.unblock(context).await; chat_id_blocked = Blocked::Not; } } if chat_id.is_unset() && allow_creation { let create_blocked = if MessengerMessage::No != msgrmsg - && !Contact::is_blocked_load(context, to_id) + && !Contact::is_blocked_load(context, to_id).await { Blocked::Not } else { Blocked::Deaddrop }; let (id, bl) = chat::create_or_lookup_by_contact_id(context, to_id, create_blocked) + .await .unwrap_or_default(); *chat_id = id; chat_id_blocked = bl; @@ -551,7 +571,7 @@ fn add_parts( && Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked { - chat_id.unblock(context); + chat_id.unblock(context).await; chat_id_blocked = Blocked::Not; } } @@ -565,12 +585,13 @@ fn add_parts( // maybe an Autocrypt Setup Message let (id, bl) = chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not) + .await .unwrap_or_default(); *chat_id = id; chat_id_blocked = bl; if !chat_id.is_unset() && Blocked::Not != chat_id_blocked { - chat_id.unblock(context); + chat_id.unblock(context).await; chat_id_blocked = Blocked::Not; } } @@ -589,14 +610,15 @@ fn add_parts( &mut sort_timestamp, sent_timestamp, &mut rcvd_timestamp, - ); + ) + .await; // unarchive chat - chat_id.unarchive(context)?; + chat_id.unarchive(context).await?; // if the mime-headers should be saved, find out its size // (the mime-header ends with an empty line) - let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders); + let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await; if let Some(raw) = mime_parser.get(HeaderDef::InReplyTo) { mime_in_reply_to = raw.clone(); } @@ -611,26 +633,47 @@ fn add_parts( // (eg. one per attachment)) let icnt = mime_parser.parts.len(); - let mut txt_raw = None; + let subject = mime_parser.get_subject().unwrap_or_default(); - context.sql.prepare( - "INSERT INTO msgs \ + let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new()); + let server_folder = server_folder.as_ref().to_string(); + let location_kml_is = mime_parser.location_kml.is_some(); + let is_system_message = mime_parser.is_system_message; + let mime_headers = if save_mime_headers { + Some(String::from_utf8_lossy(imf_raw).to_string()) + } else { + None + }; + let sent_timestamp = *sent_timestamp; + let is_hidden = *hidden; + let chat_id = *chat_id; + let is_mdn = !mime_parser.reports.is_empty(); + + // TODO: can this clone be avoided? + let rfc724_mid = rfc724_mid.to_string(); + + let (new_parts, ids, is_hidden) = context + .sql + .with_conn(move |mut conn| { + let mut ids = Vec::with_capacity(parts.len()); + let mut is_hidden = is_hidden; + + for part in &mut parts { + let mut txt_raw = "".to_string(); + let mut stmt = conn.prepare_cached( + "INSERT INTO msgs \ (rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \ timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \ bytes, hidden, mime_headers, mime_in_reply_to, mime_references) \ VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?);", - |mut stmt, conn| { - let subject = mime_parser.get_subject().unwrap_or_default(); + )?; - for part in mime_parser.parts.iter_mut() { - let is_mdn = !mime_parser.reports.is_empty(); - - let is_location_kml = mime_parser.location_kml.is_some() + let is_location_kml = location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty()); if is_mdn || is_location_kml { - *hidden = true; + is_hidden = true; if state == MessageState::InFresh { state = MessageState::InNoticed; } @@ -638,54 +681,59 @@ fn add_parts( if part.typ == Viewtype::Text { let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); - txt_raw = Some(format!("{}\n\n{}", subject, msg_raw)); + txt_raw = format!("{}\n\n{}", subject, msg_raw); } - if mime_parser.is_system_message != SystemMessage::Unknown { - part.param - .set_int(Param::Cmd, mime_parser.is_system_message as i32); + if is_system_message != SystemMessage::Unknown { + part.param.set_int(Param::Cmd, is_system_message as i32); } - stmt.execute(params![ + stmt.execute(paramsv![ rfc724_mid, - server_folder.as_ref(), + server_folder, server_uid as i32, - *chat_id, + chat_id, from_id as i32, to_id as i32, sort_timestamp, - *sent_timestamp, + sent_timestamp, rcvd_timestamp, part.typ, state, msgrmsg, - &part.msg, + part.msg, // txt_raw might contain invalid utf8 - txt_raw.unwrap_or_default(), + txt_raw, part.param.to_string(), part.bytes as isize, - *hidden, - if save_mime_headers { - Some(String::from_utf8_lossy(imf_raw)) - } else { - None - }, + is_hidden, + mime_headers, mime_in_reply_to, mime_references, ])?; - txt_raw = None; - let row_id = - sql::get_rowid_with_conn(context, conn, "msgs", "rfc724_mid", &rfc724_mid); - *insert_msg_id = MsgId::new(row_id); - created_db_entries.push((*chat_id, *insert_msg_id)); + drop(stmt); + ids.push(MsgId::new(crate::sql::get_rowid( + &mut conn, + "msgs", + "rfc724_mid", + &rfc724_mid, + )?)); } - Ok(()) - }, - )?; + Ok((parts, ids, is_hidden)) + }) + .await?; + + if let Some(id) = ids.iter().last() { + *insert_msg_id = *id; + } + + *hidden = is_hidden; + created_db_entries.extend(ids.iter().map(|id| (chat_id, *id))); + mime_parser.parts = new_parts; info!( context, - "Message has {} parts and is assigned to chat #{}.", icnt, *chat_id, + "Message has {} parts and is assigned to chat #{}.", icnt, chat_id, ); // check event to send @@ -704,7 +752,7 @@ fn add_parts( Ok(()) } -fn save_locations( +async fn save_locations( context: &Context, mime_parser: &MimeMessage, chat_id: ChatId, @@ -720,11 +768,14 @@ fn save_locations( if mime_parser.message_kml.is_some() { let locations = &mime_parser.message_kml.as_ref().unwrap().locations; - let newest_location_id = - location::save(context, chat_id, from_id, locations, true).unwrap_or_default(); + let newest_location_id = location::save(context, chat_id, from_id, locations, true) + .await + .unwrap_or_default(); if 0 != newest_location_id && !hidden - && location::set_msg_location_id(context, insert_msg_id, newest_location_id).is_ok() + && location::set_msg_location_id(context, insert_msg_id, newest_location_id) + .await + .is_ok() { location_id_written = true; send_event = true; @@ -733,18 +784,21 @@ fn save_locations( if mime_parser.location_kml.is_some() { if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr { - if let Ok(contact) = Contact::get_by_id(context, from_id) { + if let Ok(contact) = Contact::get_by_id(context, from_id).await { if contact.get_addr().to_lowercase() == addr.to_lowercase() { let locations = &mime_parser.location_kml.as_ref().unwrap().locations; let newest_location_id = location::save(context, chat_id, from_id, locations, false) + .await .unwrap_or_default(); if newest_location_id != 0 && !hidden && !location_id_written { if let Err(err) = location::set_msg_location_id( context, insert_msg_id, newest_location_id, - ) { + ) + .await + { error!(context, "Failed to set msg_location_id: {:?}", err); } } @@ -754,12 +808,12 @@ fn save_locations( } } if send_event { - context.call_cb(Event::LocationChanged(Some(from_id))); + context.emit_event(Event::LocationChanged(Some(from_id))); } } #[allow(clippy::too_many_arguments)] -fn calc_timestamps( +async fn calc_timestamps( context: &Context, chat_id: ChatId, from_id: u32, @@ -776,19 +830,22 @@ fn calc_timestamps( } *sort_timestamp = message_timestamp; if is_fresh_msg { - let last_msg_time: Option = context.sql.query_get_value( - context, - "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? and from_id!=? AND timestamp>=?", - params![chat_id, from_id as i32, *sort_timestamp], - ); + let last_msg_time: Option = context + .sql + .query_get_value( + context, + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? and from_id!=? AND timestamp>=?", + paramsv![chat_id, from_id as i32, *sort_timestamp], + ) + .await; if let Some(last_msg_time) = last_msg_time { if last_msg_time > 0 && *sort_timestamp <= last_msg_time { *sort_timestamp = last_msg_time + 1; } } } - if *sort_timestamp >= dc_smeared_time(context) { - *sort_timestamp = dc_create_smeared_timestamp(context); + if *sort_timestamp >= dc_smeared_time(context).await { + *sort_timestamp = dc_create_smeared_timestamp(context).await; } } @@ -804,7 +861,7 @@ fn calc_timestamps( /// /// on success the function returns the found/created (chat_id, chat_blocked) tuple . #[allow(non_snake_case, clippy::cognitive_complexity)] -fn create_or_lookup_group( +async fn create_or_lookup_group( context: &Context, mime_parser: &mut MimeMessage, allow_creation: bool, @@ -820,8 +877,9 @@ fn create_or_lookup_group( let mut better_msg: String = From::from(""); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { - better_msg = - context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", from_id as u32); + better_msg = context + .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", from_id as u32) + .await; set_better_msg(mime_parser, &better_msg); } @@ -851,6 +909,7 @@ fn create_or_lookup_group( from_id, to_ids, ) + .await .map_err(|err| { info!(context, "could not create adhoc-group: {:?}", err); err @@ -865,47 +924,53 @@ fn create_or_lookup_group( let mut removed_id = 0; if let Some(removed_addr) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() { - removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown); + removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await; if removed_id == 0 { warn!(context, "removed {:?} has no contact_id", removed_addr); } else { mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup; - better_msg = context.stock_system_msg( - if removed_id == from_id as u32 { - StockMessage::MsgGroupLeft - } else { - StockMessage::MsgDelMember - }, - &removed_addr, - "", - from_id as u32, - ); + better_msg = context + .stock_system_msg( + if removed_id == from_id as u32 { + StockMessage::MsgGroupLeft + } else { + StockMessage::MsgDelMember + }, + &removed_addr, + "", + from_id as u32, + ) + .await; } } else { let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned(); if let Some(optional_field) = field { mime_parser.is_system_message = SystemMessage::MemberAddedToGroup; - better_msg = context.stock_system_msg( - StockMessage::MsgAddMember, - &optional_field, - "", - from_id as u32, - ); + better_msg = context + .stock_system_msg( + StockMessage::MsgAddMember, + &optional_field, + "", + from_id as u32, + ) + .await; X_MrAddToGrp = Some(optional_field); } else { let field = mime_parser.get(HeaderDef::ChatGroupNameChanged); if let Some(field) = field { X_MrGrpNameChanged = true; - better_msg = context.stock_system_msg( - StockMessage::MsgGrpName, - field, - if let Some(ref name) = grpname { - name - } else { - "" - }, - from_id as u32, - ); + better_msg = context + .stock_system_msg( + StockMessage::MsgGrpName, + field, + if let Some(ref name) = grpname { + name + } else { + "" + }, + from_id as u32, + ) + .await; mime_parser.is_system_message = SystemMessage::GroupNameChanged; } else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) { @@ -914,15 +979,17 @@ fn create_or_lookup_group( // this is just an explicit message containing the group-avatar, // apart from that, the group-avatar is send along with various other messages mime_parser.is_system_message = SystemMessage::GroupImageChanged; - better_msg = context.stock_system_msg( - match avatar_action { - AvatarAction::Delete => StockMessage::MsgGrpImgDeleted, - AvatarAction::Change(_) => StockMessage::MsgGrpImgChanged, - }, - "", - "", - from_id as u32, - ) + better_msg = context + .stock_system_msg( + match avatar_action { + AvatarAction::Delete => StockMessage::MsgGrpImgDeleted, + AvatarAction::Change(_) => StockMessage::MsgGrpImgChanged, + }, + "", + "", + from_id as u32, + ) + .await } } } @@ -932,33 +999,37 @@ fn create_or_lookup_group( // check, if we have a chat with this group ID let (mut chat_id, chat_id_verified, _blocked) = chat::get_chat_id_by_grpid(context, &grpid) + .await .unwrap_or((ChatId::new(0), false, Blocked::Not)); if !chat_id.is_error() { if chat_id_verified { if let Err(err) = - check_verified_properties(context, mime_parser, from_id as u32, to_ids) + check_verified_properties(context, mime_parser, from_id as u32, to_ids).await { warn!(context, "verification problem: {}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(s); } } - if !chat::is_contact_in_chat(context, chat_id, from_id as u32) { + if !chat::is_contact_in_chat(context, chat_id, from_id as u32).await { // The From-address is not part of this group. // It could be a new user or a DSN from a mailer-daemon. // in any case we do not want to recreate the member list // but still show the message as part of the chat. // After all, the sender has a reference/in-reply-to that // points to this chat. - let s = context.stock_str(StockMessage::UnknownSenderForChat); + let s = context.stock_str(StockMessage::UnknownSenderForChat).await; mime_parser.repl_msg_by_error(s.to_string()); } } // check if the group does not exist but should be created - let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid).unwrap_or_default(); + let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid) + .await + .unwrap_or_default(); let self_addr = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); if chat_id.is_error() @@ -974,7 +1045,7 @@ fn create_or_lookup_group( // group does not exist but should be created let create_verified = if mime_parser.get(HeaderDef::ChatVerified).is_some() { if let Err(err) = - check_verified_properties(context, mime_parser, from_id as u32, to_ids) + check_verified_properties(context, mime_parser, from_id as u32, to_ids).await { warn!(context, "verification problem: {}", err); let s = format!("{}. See 'Info' for more details", err); @@ -996,7 +1067,8 @@ fn create_or_lookup_group( grpname.as_ref().unwrap(), create_blocked, create_verified, - ); + ) + .await; chat_id_blocked = create_blocked; recreate_member_list = true; } @@ -1014,6 +1086,7 @@ fn create_or_lookup_group( from_id, to_ids, ) + .await .map_err(|err| { warn!(context, "failed to create ad-hoc group: {:?}", err); err @@ -1040,22 +1113,23 @@ fn create_or_lookup_group( if let Some(ref grpname) = grpname { if grpname.len() < 200 { info!(context, "updating grpname for chat {}", chat_id); - if sql::execute( - context, - &context.sql, - "UPDATE chats SET name=? WHERE id=?;", - params![grpname, chat_id], - ) - .is_ok() + if context + .sql + .execute( + "UPDATE chats SET name=? WHERE id=?;", + paramsv![grpname.to_string(), chat_id], + ) + .await + .is_ok() { - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); } } } } if let Some(avatar_action) = &mime_parser.group_avatar { info!(context, "group-avatar change for {}", chat_id); - if let Ok(mut chat) = Chat::load_from_db(context, chat_id) { + if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { match avatar_action { AvatarAction::Change(profile_image) => { chat.param.set(Param::ProfileImage, profile_image); @@ -1064,49 +1138,50 @@ fn create_or_lookup_group( chat.param.remove(Param::ProfileImage); } }; - chat.update_param(context)?; + chat.update_param(context).await?; send_EVENT_CHAT_MODIFIED = true; } } // add members to group/check members if recreate_member_list { - if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) { + if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await { // Members could have been removed while we were // absent. We can't use existing member list and need to // start from scratch. - sql::execute( - context, - &context.sql, - "DELETE FROM chats_contacts WHERE chat_id=?;", - params![chat_id], - ) - .ok(); + context + .sql + .execute( + "DELETE FROM chats_contacts WHERE chat_id=?;", + paramsv![chat_id], + ) + .await + .ok(); - chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF); + chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await; } if from_id > DC_CONTACT_ID_LAST_SPECIAL - && !Contact::addr_equals_contact(context, &self_addr, from_id as u32) - && !chat::is_contact_in_chat(context, chat_id, from_id) + && !Contact::addr_equals_contact(context, &self_addr, from_id as u32).await + && !chat::is_contact_in_chat(context, chat_id, from_id).await { - chat::add_to_chat_contacts_table(context, chat_id, from_id as u32); + chat::add_to_chat_contacts_table(context, chat_id, from_id as u32).await; } for &to_id in to_ids.iter() { info!(context, "adding to={:?} to chat id={}", to_id, chat_id); - if !Contact::addr_equals_contact(context, &self_addr, to_id) - && !chat::is_contact_in_chat(context, chat_id, to_id) + if !Contact::addr_equals_contact(context, &self_addr, to_id).await + && !chat::is_contact_in_chat(context, chat_id, to_id).await { - chat::add_to_chat_contacts_table(context, chat_id, to_id); + chat::add_to_chat_contacts_table(context, chat_id, to_id).await; } } send_EVENT_CHAT_MODIFIED = true; } else if removed_id > 0 { - chat::remove_from_chat_contacts_table(context, chat_id, removed_id); + chat::remove_from_chat_contacts_table(context, chat_id, removed_id).await; send_EVENT_CHAT_MODIFIED = true; } if send_EVENT_CHAT_MODIFIED { - context.call_cb(Event::ChatModified(chat_id)); + context.emit_event(Event::ChatModified(chat_id)); } Ok((chat_id, chat_id_blocked)) } @@ -1122,7 +1197,7 @@ fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str } /// Handle groups for received messages, return chat_id/Blocked status on success -fn create_or_lookup_adhoc_group( +async fn create_or_lookup_adhoc_group( context: &Context, mime_parser: &MimeMessage, allow_creation: bool, @@ -1157,12 +1232,14 @@ fn create_or_lookup_adhoc_group( return Ok((ChatId::new(0), Blocked::Not)); } - let chat_ids = search_chat_ids_by_contact_ids(context, &member_ids)?; + let chat_ids = search_chat_ids_by_contact_ids(context, &member_ids).await?; if !chat_ids.is_empty() { let chat_ids_str = join(chat_ids.iter().map(|x| x.to_string()), ","); - let res = context.sql.query_row( - format!( - "SELECT c.id, + let res = context + .sql + .query_row( + format!( + "SELECT c.id, c.blocked FROM chats c LEFT JOIN msgs m @@ -1171,16 +1248,17 @@ fn create_or_lookup_adhoc_group( ORDER BY m.timestamp DESC, m.id DESC LIMIT 1;", - chat_ids_str - ), - params![], - |row| { - Ok(( - row.get::<_, ChatId>(0)?, - row.get::<_, Option>(1)?.unwrap_or_default(), - )) - }, - ); + chat_ids_str + ), + paramsv![], + |row| { + Ok(( + row.get::<_, ChatId>(0)?, + row.get::<_, Option>(1)?.unwrap_or_default(), + )) + }, + ) + .await; if let Ok((id, id_blocked)) = res { /* success, chat found */ @@ -1198,7 +1276,7 @@ fn create_or_lookup_adhoc_group( // create a new ad-hoc group // - there is no need to check if this group exists; otherwise we would have caught it above - let grpid = create_adhoc_grp_id(context, &member_ids); + let grpid = create_adhoc_grp_id(context, &member_ids).await; if grpid.is_empty() { warn!( context, @@ -1218,28 +1296,27 @@ fn create_or_lookup_adhoc_group( grpname, create_blocked, VerifiedStatus::Unverified, - ); + ) + .await; for &member_id in &member_ids { - chat::add_to_chat_contacts_table(context, new_chat_id, member_id); + chat::add_to_chat_contacts_table(context, new_chat_id, member_id).await; } - context.call_cb(Event::ChatModified(new_chat_id)); + context.emit_event(Event::ChatModified(new_chat_id)); Ok((new_chat_id, create_blocked)) } -fn create_group_record( +async fn create_group_record( context: &Context, grpid: impl AsRef, grpname: impl AsRef, create_blocked: Blocked, create_verified: VerifiedStatus, ) -> ChatId { - if sql::execute( - context, - &context.sql, + if context.sql.execute( "INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);", - params![ + paramsv![ if VerifiedStatus::Unverified != create_verified { Chattype::VerifiedGroup } else { @@ -1250,7 +1327,7 @@ fn create_group_record( create_blocked, time(), ], - ) + ).await .is_err() { warn!( @@ -1261,7 +1338,12 @@ fn create_group_record( ); return ChatId::new(0); } - let row_id = sql::get_rowid(context, &context.sql, "chats", "grpid", grpid.as_ref()); + let row_id = context + .sql + .get_rowid(context, "chats", "grpid", grpid.as_ref()) + .await + .unwrap_or_default(); + let chat_id = ChatId::new(row_id); info!( context, @@ -1273,7 +1355,7 @@ fn create_group_record( chat_id } -fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { +async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { /* algorithm: - sort normalized, lowercased, e-mail addresses alphabetically - put all e-mail addresses into a single string, separate the address by a single comma @@ -1283,6 +1365,7 @@ fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { let member_ids_str = join(member_ids.iter().map(|x| x.to_string()), ","); let member_cs = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_else(|| "no-self".to_string()) .to_lowercase(); @@ -1293,7 +1376,7 @@ fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { "SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF member_ids_str ), - params![], + paramsv![], |row| row.get::<_, String>(0), |rows| { let mut addrs = rows.collect::, _>>()?; @@ -1306,6 +1389,7 @@ fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { Ok(acc) }, ) + .await .unwrap_or_else(|_| member_cs); hex_hash(&members) @@ -1317,7 +1401,7 @@ fn hex_hash(s: impl AsRef) -> String { hex::encode(&result[..8]) } -fn search_chat_ids_by_contact_ids( +async fn search_chat_ids_by_contact_ids( context: &Context, unsorted_contact_ids: &[u32], ) -> Result> { @@ -1346,7 +1430,7 @@ fn search_chat_ids_by_contact_ids( ORDER BY cc.chat_id, cc.contact_id;", // 1=DC_CONTACT_ID_SELF contact_ids_str ), - params![], + paramsv![], |row| Ok((row.get::<_, ChatId>(0)?, row.get::<_, u32>(1)?)), |rows| { let mut last_chat_id = ChatId::new(0); @@ -1375,20 +1459,20 @@ fn search_chat_ids_by_contact_ids( } Ok(()) } - )?; + ).await?; } } Ok(chat_ids) } -fn check_verified_properties( +async fn check_verified_properties( context: &Context, mimeparser: &MimeMessage, from_id: u32, to_ids: &ContactIds, ) -> Result<()> { - let contact = Contact::load_from_db(context, from_id)?; + let contact = Contact::load_from_db(context, from_id).await?; ensure!(mimeparser.was_encrypted(), "This message is not encrypted."); @@ -1397,10 +1481,10 @@ fn check_verified_properties( // this check is skipped for SELF as there is no proper SELF-peerstate // and results in group-splits otherwise. if from_id != DC_CONTACT_ID_SELF { - let peerstate = Peerstate::from_addr(context, &context.sql, contact.get_addr()); + let peerstate = Peerstate::from_addr(context, contact.get_addr()).await; if peerstate.is_none() - || contact.is_verified_ex(context, peerstate.as_ref()) + || contact.is_verified_ex(context, peerstate.as_ref()).await != VerifiedStatus::BidirectVerified { bail!( @@ -1426,29 +1510,32 @@ fn check_verified_properties( } let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ","); - let rows = context.sql.query_map( - format!( - "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \ + let rows = context + .sql + .query_map( + format!( + "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ", - to_ids_str - ), - params![], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1).unwrap_or(0))), - |rows| { - rows.collect::, _>>() - .map_err(Into::into) - }, - )?; + to_ids_str + ), + paramsv![], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1).unwrap_or(0))), + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; for (to_addr, _is_verified) in rows.into_iter() { info!( context, "check_verified_properties: {:?} self={:?}", to_addr, - context.is_self_addr(&to_addr) + context.is_self_addr(&to_addr).await ); let mut is_verified = _is_verified != 0; - let peerstate = Peerstate::from_addr(context, &context.sql, &to_addr); + let peerstate = Peerstate::from_addr(context, &to_addr).await; // mark gossiped keys (if any) as verified if mimeparser.gossipped_addr.contains(&to_addr) { @@ -1470,7 +1557,7 @@ fn check_verified_properties( &fp, PeerstateVerifiedStatus::BidirectVerified, ); - peerstate.save_to_db(&context.sql, false)?; + peerstate.save_to_db(&context.sql, false).await?; is_verified = true; } } @@ -1496,18 +1583,18 @@ fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef) { } } -fn is_reply_to_known_message(context: &Context, mime_parser: &MimeMessage) -> bool { +async fn is_reply_to_known_message(context: &Context, mime_parser: &MimeMessage) -> bool { /* check if the message is a reply to a known message; the replies are identified by the Message-ID from `In-Reply-To`/`References:` (to support non-Delta-Clients) */ if let Some(field) = mime_parser.get(HeaderDef::InReplyTo) { - if is_known_rfc724_mid_in_list(context, &field) { + if is_known_rfc724_mid_in_list(context, &field).await { return true; } } if let Some(field) = mime_parser.get(HeaderDef::References) { - if is_known_rfc724_mid_in_list(context, &field) { + if is_known_rfc724_mid_in_list(context, &field).await { return true; } } @@ -1515,14 +1602,14 @@ fn is_reply_to_known_message(context: &Context, mime_parser: &MimeMessage) -> bo false } -fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { +async fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { if mid_list.is_empty() { return false; } if let Ok(ids) = parse_message_ids(mid_list) { for id in ids.iter() { - if is_known_rfc724_mid(context, id) { + if is_known_rfc724_mid(context, id).await { return true; } } @@ -1532,8 +1619,9 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { } /// Check if a message is a reply to a known message (messenger or non-messenger). -fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { +async fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>'); + context .sql .exists( @@ -1541,8 +1629,9 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { LEFT JOIN chats c ON m.chat_id=c.id \ WHERE m.rfc724_mid=? \ AND m.chat_id>9 AND c.blocked=0;", - params![rfc724_mid], + paramsv![rfc724_mid], ) + .await .unwrap_or_default() } @@ -1551,15 +1640,15 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { /// - checks also if any of the referenced IDs are send by a messenger /// - it is okay, if the referenced messages are moved to trash here /// - no check for the Chat-* headers (function is only called if it is no messenger message itself) -fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -> bool { +async fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -> bool { if let Some(value) = mime_parser.get(HeaderDef::InReplyTo) { - if is_msgrmsg_rfc724_mid_in_list(context, &value) { + if is_msgrmsg_rfc724_mid_in_list(context, &value).await { return true; } } if let Some(value) = mime_parser.get(HeaderDef::References) { - if is_msgrmsg_rfc724_mid_in_list(context, &value) { + if is_msgrmsg_rfc724_mid_in_list(context, &value).await { return true; } } @@ -1567,10 +1656,10 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) - false } -pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { +pub(crate) async fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { if let Ok(ids) = parse_message_ids(mid_list) { for id in ids.iter() { - if is_msgrmsg_rfc724_mid(context, id) { + if is_msgrmsg_rfc724_mid(context, id).await { return true; } } @@ -1579,43 +1668,42 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) - } /// Check if a message is a reply to any messenger message. -fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { +async fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool { let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>'); + context .sql .exists( "SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;", - params![rfc724_mid], + paramsv![rfc724_mid], ) + .await .unwrap_or_default() } -fn dc_add_or_lookup_contacts_by_address_list( +async fn dc_add_or_lookup_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], origin: Origin, ) -> Result { let mut contact_ids = ContactIds::new(); for info in address_list.iter() { - contact_ids.insert(add_or_lookup_contact_by_addr( - context, - &info.display_name, - &info.addr, - origin, - )?); + contact_ids.insert( + add_or_lookup_contact_by_addr(context, &info.display_name, &info.addr, origin).await?, + ); } Ok(contact_ids) } /// Add contacts to database on receiving messages. -fn add_or_lookup_contact_by_addr( +async fn add_or_lookup_contact_by_addr( context: &Context, display_name: &Option, addr: &str, origin: Origin, ) -> Result { - if context.is_self_addr(addr)? { + if context.is_self_addr(addr).await? { return Ok(DC_CONTACT_ID_SELF); } let display_name_normalized = display_name @@ -1624,7 +1712,7 @@ fn add_or_lookup_contact_by_addr( .unwrap_or_default(); let (row_id, _modified) = - Contact::add_or_lookup(context, display_name_normalized, addr, origin)?; + Contact::add_or_lookup(context, display_name_normalized, addr, origin).await?; ensure!(row_id > 0, "could not add contact: {:?}", addr); Ok(row_id) @@ -1662,31 +1750,35 @@ mod tests { assert_eq!(res, "b94d27b9934d3e08"); } - #[test] - fn test_grpid_simple() { - let context = dummy_context(); + #[async_std::test] + async fn test_grpid_simple() { + let context = dummy_context().await; let raw = b"From: hello\n\ Subject: outer-subject\n\ In-Reply-To: \n\ References: \n\ \n\ hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); let grpid = Some("HcxyMARjyJy"); assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); } - #[test] - fn test_grpid_from_multiple() { - let context = dummy_context(); + #[async_std::test] + async fn test_grpid_from_multiple() { + let context = dummy_context().await; let raw = b"From: hello\n\ Subject: outer-subject\n\ In-Reply-To: \n\ References: , \n\ \n\ hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); let grpid = Some("HcxyMARjyJy"); assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid); assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); @@ -1712,49 +1804,52 @@ mod tests { ); } - #[test] - fn test_is_known_rfc724_mid() { - let t = dummy_context(); + #[async_std::test] + async fn test_is_known_rfc724_mid() { + let t = dummy_context().await; let mut msg = Message::new(Viewtype::Text); msg.text = Some("first message".to_string()); - let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); - let msg = Message::load_from_db(&t.ctx, msg_id).unwrap(); + let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)) + .await + .unwrap(); + let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap(); // Message-IDs may or may not be surrounded by angle brackets - assert!(is_known_rfc724_mid( - &t.ctx, - format!("<{}>", msg.rfc724_mid).as_str() - )); - assert!(is_known_rfc724_mid(&t.ctx, &msg.rfc724_mid)); - assert!(!is_known_rfc724_mid(&t.ctx, "nonexistant@message.id")); + assert!(is_known_rfc724_mid(&t.ctx, format!("<{}>", msg.rfc724_mid).as_str()).await); + assert!(is_known_rfc724_mid(&t.ctx, &msg.rfc724_mid).await); + assert!(!is_known_rfc724_mid(&t.ctx, "nonexistant@message.id").await); } - #[test] - fn test_is_msgrmsg_rfc724_mid() { - let t = dummy_context(); + #[async_std::test] + async fn test_is_msgrmsg_rfc724_mid() { + let t = dummy_context().await; let mut msg = Message::new(Viewtype::Text); msg.text = Some("first message".to_string()); - let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap(); - let msg = Message::load_from_db(&t.ctx, msg_id).unwrap(); + let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)) + .await + .unwrap(); + let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap(); // Message-IDs may or may not be surrounded by angle brackets - assert!(is_msgrmsg_rfc724_mid( - &t.ctx, - format!("<{}>", msg.rfc724_mid).as_str() - )); - assert!(is_msgrmsg_rfc724_mid(&t.ctx, &msg.rfc724_mid)); - assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id")); + assert!(is_msgrmsg_rfc724_mid(&t.ctx, format!("<{}>", msg.rfc724_mid).as_str()).await); + assert!(is_msgrmsg_rfc724_mid(&t.ctx, &msg.rfc724_mid).await); + assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id").await); } - fn configured_offline_context() -> TestContext { - let t = dummy_context(); + async fn configured_offline_context() -> TestContext { + let t = dummy_context().await; t.ctx .set_config(Config::Addr, Some("alice@example.org")) + .await .unwrap(); t.ctx .set_config(Config::ConfiguredAddr, Some("alice@example.org")) + .await + .unwrap(); + t.ctx + .set_config(Config::Configured, Some("1")) + .await .unwrap(); - t.ctx.set_config(Config::Configured, Some("1")).unwrap(); t } @@ -1783,128 +1878,176 @@ mod tests { \n\ hello\n"; - #[test] - fn test_adhoc_group_show_chats_only() { - let t = configured_offline_context(); - assert_eq!(t.ctx.get_config_int(Config::ShowEmails), 0); + #[async_std::test] + async fn test_adhoc_group_show_chats_only() { + let t = configured_offline_context().await; + assert_eq!(t.ctx.get_config_int(Config::ShowEmails).await, 0); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); - dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); - dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false).unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); - dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); } - #[test] - fn test_adhoc_group_show_accepted_contact_unknown() { - let t = configured_offline_context(); - t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap(); - dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap(); + #[async_std::test] + async fn test_adhoc_group_show_accepted_contact_unknown() { + let t = configured_offline_context().await; + t.ctx + .set_config(Config::ShowEmails, Some("1")) + .await + .unwrap(); + dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false) + .await + .unwrap(); // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); } - #[test] - fn test_adhoc_group_show_accepted_contact_known() { - let t = configured_offline_context(); - t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap(); - Contact::create(&t.ctx, "Bob", "bob@example.org").unwrap(); - dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap(); + #[async_std::test] + async fn test_adhoc_group_show_accepted_contact_known() { + let t = configured_offline_context().await; + t.ctx + .set_config(Config::ShowEmails, Some("1")) + .await + .unwrap(); + Contact::create(&t.ctx, "Bob", "bob@example.org") + .await + .unwrap(); + dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false) + .await + .unwrap(); // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts // (and existent chat is required) - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); } - #[test] - fn test_adhoc_group_show_accepted_contact_accepted() { - let t = configured_offline_context(); - t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap(); + #[async_std::test] + async fn test_adhoc_group_show_accepted_contact_accepted() { + let t = configured_offline_context().await; + t.ctx + .set_config(Config::ShowEmails, Some("1")) + .await + .unwrap(); // accept Bob by accepting a delta-message from Bob - dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); assert!(chats.get_chat_id(0).is_deaddrop()); - let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap(); + let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()) + .await + .unwrap(); assert!(!chat_id.is_special()); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap(); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); assert_eq!(chat.typ, Chattype::Single); assert_eq!(chat.name, "Bob"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 1); - assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 1); + assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 1); + assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 1); // receive a non-delta-message from Bob, shows up because of the show_emails setting - dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false).unwrap(); - assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 2); + dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false) + .await + .unwrap(); + assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 2); // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting - dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false).unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false) + .await + .unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 2); - let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap(); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap(); + let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()) + .await + .unwrap(); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); assert_eq!(chat.typ, Chattype::Group); assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3); + assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3); } - #[test] - fn test_adhoc_group_show_all() { - let t = configured_offline_context(); - t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap(); - dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap(); + #[async_std::test] + async fn test_adhoc_group_show_all() { + let t = configured_offline_context().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false) + .await + .unwrap(); // adhoc-group with unknown contacts with show_emails=all will show up in the deaddrop - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); assert!(chats.get_chat_id(0).is_deaddrop()); - let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap(); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap(); + let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()) + .await + .unwrap(); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); assert_eq!(chat.typ, Chattype::Group); assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3); + assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3); } - #[test] - fn test_read_receipt_and_unarchive() { + #[async_std::test] + async fn test_read_receipt_and_unarchive() { // create alice's account - let t = configured_offline_context(); + let t = configured_offline_context().await; // create one-to-one with bob, archive one-to-one - let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org").unwrap(); - let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).unwrap(); + let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org") + .await + .unwrap(); + let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).await.unwrap(); one2one_id .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .unwrap(); - let one2one = Chat::load_from_db(&t.ctx, one2one_id).unwrap(); + let one2one = Chat::load_from_db(&t.ctx, one2one_id).await.unwrap(); assert!(one2one.get_visibility() == ChatVisibility::Archived); // create a group with bob, archive group - let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); - chat::add_contact_to_chat(&t.ctx, group_id, bob_id); - assert_eq!(chat::get_chat_msgs(&t.ctx, group_id, 0, None).len(), 0); + let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo") + .await + .unwrap(); + chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await; + assert_eq!( + chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(), + 0 + ); group_id .set_visibility(&t.ctx, ChatVisibility::Archived) + .await .unwrap(); - let group = Chat::load_from_db(&t.ctx, group_id).unwrap(); + let group = Chat::load_from_db(&t.ctx, group_id).await.unwrap(); assert!(group.get_visibility() == ChatVisibility::Archived); // everything archived, chatlist should be empty assert_eq!( Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None) + .await .unwrap() .len(), 0 @@ -1932,15 +2075,18 @@ mod tests { 1, false, ) + .await .unwrap(); - let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None); + let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await; assert_eq!(msgs.len(), 1); let msg_id = msgs.first().unwrap(); - let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap(); + let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()) + .await + .unwrap(); assert_eq!(msg.is_dc_message, MessengerMessage::Yes); assert_eq!(msg.text.unwrap(), "hello"); assert_eq!(msg.state, MessageState::OutDelivered); - let group = Chat::load_from_db(&t.ctx, group_id).unwrap(); + let group = Chat::load_from_db(&t.ctx, group_id).await.unwrap(); assert!(group.get_visibility() == ChatVisibility::Normal); // bob sends a read receipt to the group @@ -1980,32 +2126,38 @@ mod tests { 1, false, ) - .unwrap(); - assert_eq!(chat::get_chat_msgs(&t.ctx, group_id, 0, None).len(), 1); - let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap(); + .await.unwrap(); + assert_eq!( + chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(), + 1 + ); + let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()) + .await + .unwrap(); assert_eq!(msg.state, MessageState::OutMdnRcvd); // check, the read-receipt has not unarchived the one2one assert_eq!( Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None) + .await .unwrap() .len(), 1 ); - let one2one = Chat::load_from_db(&t.ctx, one2one_id).unwrap(); + let one2one = Chat::load_from_db(&t.ctx, one2one_id).await.unwrap(); assert!(one2one.get_visibility() == ChatVisibility::Archived); } - #[test] - fn test_no_from() { + #[async_std::test] + async fn test_no_from() { // if there is no from given, from_id stays 0 which is just fine. These messages // are very rare, however, we have to add them to the database (they go to the // "deaddrop" chat) to avoid a re-download from the server. See also [**] - let t = configured_offline_context(); + let t = configured_offline_context().await; let context = &t.ctx; - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert!(chats.get_msg_id(0).is_err()); dc_receive_imf( @@ -2021,18 +2173,23 @@ mod tests { 1, false, ) + .await .unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); // Check that the message was added to the database: assert!(chats.get_msg_id(0).is_ok()); } - #[test] - fn test_escaped_from() { - let t = configured_offline_context(); - let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap(); - let chat_id = chat::create_by_contact_id(&t.ctx, contact_id).unwrap(); + #[async_std::test] + async fn test_escaped_from() { + let t = configured_offline_context().await; + let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com") + .await + .unwrap(); + let chat_id = chat::create_by_contact_id(&t.ctx, contact_id) + .await + .unwrap(); dc_receive_imf( &t.ctx, b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ @@ -2047,29 +2204,35 @@ mod tests { "INBOX", 1, false, - ).unwrap(); + ).await.unwrap(); assert_eq!( Contact::load_from_db(&t.ctx, contact_id) + .await .unwrap() .get_authname(), "Фамилия Имя", // The name was "Имя, Фамилия" and ("lastname, firstname") and should be swapped to "firstname, lastname" ); - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None); + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await; assert_eq!(msgs.len(), 1); let msg_id = msgs.first().unwrap(); - let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()).unwrap(); + let msg = message::Message::load_from_db(&t.ctx, msg_id.clone()) + .await + .unwrap(); assert_eq!(msg.is_dc_message, MessengerMessage::Yes); assert_eq!(msg.text.unwrap(), "hello"); assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); } - #[test] - fn test_escaped_recipients() { - let t = configured_offline_context(); - Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap(); + #[async_std::test] + async fn test_escaped_recipients() { + let t = configured_offline_context().await; + Contact::create(&t.ctx, "foobar", "foobar@example.com") + .await + .unwrap(); let carl_contact_id = Contact::add_or_lookup(&t.ctx, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) + .await .unwrap() .0; @@ -2089,25 +2252,31 @@ mod tests { 1, false, ) + .await .unwrap(); assert_eq!( Contact::load_from_db(&t.ctx, carl_contact_id) + .await .unwrap() .get_name(), "h2" ); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); - let msg = Message::load_from_db(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + let msg = Message::load_from_db(&t.ctx, chats.get_msg_id(0).unwrap()) + .await + .unwrap(); assert_eq!(msg.is_dc_message, MessengerMessage::Yes); assert_eq!(msg.text.unwrap(), "hello"); assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); } - #[test] - fn test_cc_to_contact() { - let t = configured_offline_context(); - Contact::create(&t.ctx, "foobar", "foobar@example.com").unwrap(); + #[async_std::test] + async fn test_cc_to_contact() { + let t = configured_offline_context().await; + Contact::create(&t.ctx, "foobar", "foobar@example.com") + .await + .unwrap(); let carl_contact_id = Contact::add_or_lookup( &t.ctx, @@ -2115,6 +2284,7 @@ mod tests { "carl@host.tld", Origin::IncomingUnknownFrom, ) + .await .unwrap() .0; @@ -2134,9 +2304,11 @@ mod tests { 1, false, ) + .await .unwrap(); assert_eq!( Contact::load_from_db(&t.ctx, carl_contact_id) + .await .unwrap() .get_name(), "Carl" diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 1adaa5ed8..63fe7626f 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -3,11 +3,12 @@ use core::cmp::{max, min}; use std::borrow::Cow; -use std::path::{Path, PathBuf}; +use std::fmt; use std::str::FromStr; use std::time::{Duration, SystemTime}; -use std::{fmt, fs}; +use async_std::path::{Path, PathBuf}; +use async_std::{fs, io}; use chrono::{Local, TimeZone}; use rand::{thread_rng, Rng}; @@ -107,9 +108,9 @@ const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5; // returns the currently smeared timestamp, // may be used to check if call to dc_create_smeared_timestamp() is needed or not. // the returned timestamp MUST NOT be used to be sent out or saved in the database! -pub(crate) fn dc_smeared_time(context: &Context) -> i64 { +pub(crate) async fn dc_smeared_time(context: &Context) -> i64 { let mut now = time(); - let ts = *context.last_smeared_timestamp.read().unwrap(); + let ts = *context.last_smeared_timestamp.read().await; if ts >= now { now = ts + 1; } @@ -118,11 +119,11 @@ pub(crate) fn dc_smeared_time(context: &Context) -> i64 { } // returns a timestamp that is guaranteed to be unique. -pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 { +pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 { let now = time(); let mut ret = now; - let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap(); + let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await; if ret <= *last_smeared_timestamp { ret = *last_smeared_timestamp + 1; if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE { @@ -137,12 +138,12 @@ pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 { // creates `count` timestamps that are guaranteed to be unique. // the frist created timestamps is returned directly, // get the other timestamps just by adding 1..count-1 -pub(crate) fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 { +pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 { let now = time(); let count = count as i64; let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count; - let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap(); + let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await; start = max(*last_smeared_timestamp + 1, start); *last_smeared_timestamp = start + count - 1; @@ -248,11 +249,8 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> { /// /// If `path` starts with "$BLOBDIR", replaces it with the blobdir path. /// Otherwise, returns path as is. -pub(crate) fn dc_get_abs_path>( - context: &Context, - path: P, -) -> std::path::PathBuf { - let p: &std::path::Path = path.as_ref(); +pub(crate) fn dc_get_abs_path>(context: &Context, path: P) -> PathBuf { + let p: &Path = path.as_ref(); if let Ok(p) = p.strip_prefix("$BLOBDIR") { context.get_blobdir().join(p) } else { @@ -260,20 +258,20 @@ pub(crate) fn dc_get_abs_path>( } } -pub(crate) fn dc_get_filebytes(context: &Context, path: impl AsRef) -> u64 { +pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef) -> u64 { let path_abs = dc_get_abs_path(context, &path); - match fs::metadata(&path_abs) { + match fs::metadata(&path_abs).await { Ok(meta) => meta.len() as u64, Err(_err) => 0, } } -pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef) -> bool { +pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef) -> bool { let path_abs = dc_get_abs_path(context, &path); - if !path_abs.exists() { + if !path_abs.exists().await { return false; } - if !path_abs.is_file() { + if !path_abs.is_file().await { warn!( context, "refusing to delete non-file \"{}\".", @@ -283,9 +281,9 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef { - context.call_cb(Event::DeletedBlobFile(dpath)); + context.emit_event(Event::DeletedBlobFile(dpath)); true } Err(err) => { @@ -295,13 +293,13 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef, - dest_path: impl AsRef, + src_path: impl AsRef, + dest_path: impl AsRef, ) -> bool { let src_abs = dc_get_abs_path(context, &src_path); - let mut src_file = match fs::File::open(&src_abs) { + let mut src_file = match fs::File::open(&src_abs).await { Ok(file) => file, Err(err) => { warn!( @@ -319,6 +317,7 @@ pub(crate) fn dc_copy_file( .create_new(true) .write(true) .open(&dest_abs) + .await { Ok(file) => file, Err(err) => { @@ -332,7 +331,7 @@ pub(crate) fn dc_copy_file( } }; - match std::io::copy(&mut src_file, &mut dest_file) { + match io::copy(&mut src_file, &mut dest_file).await { Ok(_) => true, Err(err) => { error!( @@ -344,20 +343,20 @@ pub(crate) fn dc_copy_file( ); { // Attempt to remove the failed file, swallow errors resulting from that. - fs::remove_file(dest_abs).ok(); + fs::remove_file(dest_abs).await.ok(); } false } } } -pub(crate) fn dc_create_folder( +pub(crate) async fn dc_create_folder( context: &Context, - path: impl AsRef, -) -> Result<(), std::io::Error> { + path: impl AsRef, +) -> Result<(), io::Error> { let path_abs = dc_get_abs_path(context, &path); - if !path_abs.exists() { - match fs::create_dir_all(path_abs) { + if !path_abs.exists().await { + match fs::create_dir_all(path_abs).await { Ok(_) => Ok(()), Err(err) => { warn!( @@ -375,13 +374,13 @@ pub(crate) fn dc_create_folder( } /// Write a the given content to provied file path. -pub(crate) fn dc_write_file( +pub(crate) async fn dc_write_file( context: &Context, path: impl AsRef, buf: &[u8], -) -> Result<(), std::io::Error> { +) -> Result<(), io::Error> { let path_abs = dc_get_abs_path(context, &path); - fs::write(&path_abs, buf).map_err(|err| { + fs::write(&path_abs, buf).await.map_err(|err| { warn!( context, "Cannot write {} bytes to \"{}\": {}", @@ -393,13 +392,10 @@ pub(crate) fn dc_write_file( }) } -pub fn dc_read_file>( - context: &Context, - path: P, -) -> Result, Error> { +pub async fn dc_read_file>(context: &Context, path: P) -> Result, Error> { let path_abs = dc_get_abs_path(context, &path); - match fs::read(&path_abs) { + match fs::read(&path_abs).await { Ok(bytes) => Ok(bytes), Err(err) => { warn!( @@ -413,13 +409,31 @@ pub fn dc_read_file>( } } -pub fn dc_open_file>( +pub async fn dc_open_file>(context: &Context, path: P) -> Result { + let path_abs = dc_get_abs_path(context, &path); + + match fs::File::open(&path_abs).await { + Ok(bytes) => Ok(bytes), + Err(err) => { + warn!( + context, + "Cannot read \"{}\" or file is empty: {}", + path.as_ref().display(), + err + ); + Err(err.into()) + } + } +} + +pub fn dc_open_file_std>( context: &Context, path: P, ) -> Result { - let path_abs = dc_get_abs_path(context, &path); + let p: PathBuf = path.as_ref().into(); + let path_abs = dc_get_abs_path(context, p); - match fs::File::open(&path_abs) { + match std::fs::File::open(&path_abs) { Ok(bytes) => Ok(bytes), Err(err) => { warn!( @@ -433,7 +447,7 @@ pub fn dc_open_file>( } } -pub(crate) fn dc_get_next_backup_path( +pub(crate) async fn dc_get_next_backup_path( folder: impl AsRef, backup_time: i64, ) -> Result { @@ -446,7 +460,7 @@ pub(crate) fn dc_get_next_backup_path( for i in 0..64 { let mut path = folder.clone(); path.push(format!("{}-{}.bak", stem, i)); - if !path.exists() { + if !path.exists().await { return Ok(path); } } @@ -760,31 +774,35 @@ mod tests { } } - #[test] - fn test_file_handling() { - let t = dummy_context(); + #[async_std::test] + async fn test_file_handling() { + let t = dummy_context().await; let context = &t.ctx; - let dc_file_exist = |ctx: &Context, fname: &str| { - ctx.get_blobdir() - .join(Path::new(fname).file_name().unwrap()) - .exists() - }; - - assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje")); - if dc_file_exist(context, "$BLOBDIR/foobar") - || dc_file_exist(context, "$BLOBDIR/dada") - || dc_file_exist(context, "$BLOBDIR/foobar.dadada") - || dc_file_exist(context, "$BLOBDIR/foobar-folder") - { - dc_delete_file(context, "$BLOBDIR/foobar"); - dc_delete_file(context, "$BLOBDIR/dada"); - dc_delete_file(context, "$BLOBDIR/foobar.dadada"); - dc_delete_file(context, "$BLOBDIR/foobar-folder"); + macro_rules! dc_file_exist { + ($ctx:expr, $fname:expr) => { + $ctx.get_blobdir() + .join(Path::new($fname).file_name().unwrap()) + .exists() + }; } - assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok()); - assert!(dc_file_exist(context, "$BLOBDIR/foobar",)); - assert!(!dc_file_exist(context, "$BLOBDIR/foobarx")); - assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7); + + assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await); + if dc_file_exist!(context, "$BLOBDIR/foobar").await + || dc_file_exist!(context, "$BLOBDIR/dada").await + || dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await + || dc_file_exist!(context, "$BLOBDIR/foobar-folder").await + { + dc_delete_file(context, "$BLOBDIR/foobar").await; + dc_delete_file(context, "$BLOBDIR/dada").await; + dc_delete_file(context, "$BLOBDIR/foobar.dadada").await; + dc_delete_file(context, "$BLOBDIR/foobar-folder").await; + } + assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content") + .await + .is_ok()); + assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await); + assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await); + assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7); let abs_path = context .get_blobdir() @@ -792,31 +810,33 @@ mod tests { .to_string_lossy() .to_string(); - assert!(dc_file_exist(context, &abs_path)); + assert!(dc_file_exist!(context, &abs_path).await); - assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); + assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await); // attempting to copy a second time should fail - assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); + assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await); - assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7); + assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7); - let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap(); + let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap(); assert_eq!(buf.len(), 7); assert_eq!(&buf, b"content"); - assert!(dc_delete_file(context, "$BLOBDIR/foobar")); - assert!(dc_delete_file(context, "$BLOBDIR/dada")); - assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok()); - assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",)); - assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder")); + assert!(dc_delete_file(context, "$BLOBDIR/foobar").await); + assert!(dc_delete_file(context, "$BLOBDIR/dada").await); + assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder") + .await + .is_ok()); + assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await); + assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await); let fn0 = "$BLOBDIR/data.data"; - assert!(dc_write_file(context, &fn0, b"content").is_ok()); + assert!(dc_write_file(context, &fn0, b"content").await.is_ok()); - assert!(dc_delete_file(context, &fn0)); - assert!(!dc_file_exist(context, &fn0)); + assert!(dc_delete_file(context, &fn0).await); + assert!(!dc_file_exist!(context, &fn0).await); } #[test] @@ -833,15 +853,15 @@ mod tests { assert!(!listflags_has(listflags, DC_GCL_ADD_SELF)); } - #[test] - fn test_create_smeared_timestamp() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_smeared_timestamp() { + let t = dummy_context().await; assert_ne!( - dc_create_smeared_timestamp(&t.ctx), - dc_create_smeared_timestamp(&t.ctx) + dc_create_smeared_timestamp(&t.ctx).await, + dc_create_smeared_timestamp(&t.ctx).await ); assert!( - dc_create_smeared_timestamp(&t.ctx) + dc_create_smeared_timestamp(&t.ctx).await >= SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -849,17 +869,17 @@ mod tests { ); } - #[test] - fn test_create_smeared_timestamps() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_smeared_timestamps() { + let t = dummy_context().await; let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1; - let start = dc_create_smeared_timestamps(&t.ctx, count as usize); - let next = dc_smeared_time(&t.ctx); + let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await; + let next = dc_smeared_time(&t.ctx).await; assert!((start + count - 1) < next); let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30; - let start = dc_create_smeared_timestamps(&t.ctx, count as usize); - let next = dc_smeared_time(&t.ctx); + let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await; + let next = dc_smeared_time(&t.ctx).await; assert!((start + count - 1) < next); } diff --git a/src/e2ee.rs b/src/e2ee.rs index 60ec042da..bb3f985d3 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -25,18 +25,18 @@ pub struct EncryptHelper { } impl EncryptHelper { - pub fn new(context: &Context) -> Result { + pub async fn new(context: &Context) -> Result { let prefer_encrypt = - EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled)) + EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await) .unwrap_or_default(); - let addr = match context.get_config(Config::ConfiguredAddr) { + let addr = match context.get_config(Config::ConfiguredAddr).await { None => { bail!("addr not configured!"); } Some(addr) => addr, }; - let public_key = SignedPublicKey::load_self(context)?; + let public_key = SignedPublicKey::load_self(context).await?; Ok(EncryptHelper { prefer_encrypt, @@ -86,37 +86,37 @@ impl EncryptHelper { } /// Tries to encrypt the passed in `mail`. - pub fn encrypt( - &mut self, + pub async fn encrypt( + self, context: &Context, min_verified: PeerstateVerifiedStatus, mail_to_encrypt: lettre_email::PartBuilder, - peerstates: &[(Option, &str)], + peerstates: Vec<(Option>, &str)>, ) -> Result { let mut keyring = Keyring::default(); for (peerstate, addr) in peerstates - .iter() - .filter_map(|(state, addr)| state.as_ref().map(|s| (s, addr))) + .into_iter() + .filter_map(|(state, addr)| state.map(|s| (s, addr))) { - let key = peerstate.peek_key(min_verified).ok_or_else(|| { + let key = peerstate.take_key(min_verified).ok_or_else(|| { format_err!("proper enc-key for {} missing, cannot encrypt", addr) })?; - keyring.add_ref(key); + keyring.add(key); } - let public_key = Key::from(self.public_key.clone()); - keyring.add_ref(&public_key); - let sign_key = Key::from(SignedSecretKey::load_self(context)?); + let public_key = Key::from(self.public_key); + keyring.add(public_key); + let sign_key = Key::from(SignedSecretKey::load_self(context).await?); let raw_message = mail_to_encrypt.build().as_string().into_bytes(); - let ctext = pgp::pk_encrypt(&raw_message, &keyring, Some(&sign_key))?; + let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?; Ok(ctext) } } -pub fn try_decrypt( +pub async fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, message_time: i64, @@ -133,54 +133,56 @@ pub fn try_decrypt( let autocryptheader = Aheader::from_headers(context, &from, &mail.headers); if message_time > 0 { - peerstate = Peerstate::from_addr(context, &context.sql, &from); + peerstate = Peerstate::from_addr(context, &from).await; if let Some(ref mut peerstate) = peerstate { if let Some(ref header) = autocryptheader { peerstate.apply_header(&header, message_time); - peerstate.save_to_db(&context.sql, false)?; + peerstate.save_to_db(&context.sql, false).await?; } else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) { peerstate.degrade_encryption(message_time); - peerstate.save_to_db(&context.sql, false)?; + peerstate.save_to_db(&context.sql, false).await?; } } else if let Some(ref header) = autocryptheader { let p = Peerstate::from_header(context, header, message_time); - p.save_to_db(&context.sql, true)?; + p.save_to_db(&context.sql, true).await?; peerstate = Some(p); } } /* possibly perform decryption */ - let mut private_keyring = Keyring::default(); let mut public_keyring_for_validate = Keyring::default(); let mut out_mail = None; let mut signatures = HashSet::default(); - let self_addr = context.get_config(Config::ConfiguredAddr); + let self_addr = context.get_config(Config::ConfiguredAddr).await; if let Some(self_addr) = self_addr { - if private_keyring.load_self_private_for_decrypting(context, self_addr, &context.sql) { + if let Ok(private_keyring) = + Keyring::load_self_private_for_decrypting(context, self_addr).await + { if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 { - peerstate = Peerstate::from_addr(&context, &context.sql, &from); + peerstate = Peerstate::from_addr(&context, &from).await; } - if let Some(ref peerstate) = peerstate { + if let Some(peerstate) = peerstate { if peerstate.degrade_event.is_some() { - handle_degrade_event(context, &peerstate)?; + handle_degrade_event(context, &peerstate).await?; } - if let Some(ref key) = peerstate.gossip_key { - public_keyring_for_validate.add_ref(key); + if let Some(key) = peerstate.gossip_key { + public_keyring_for_validate.add(key); } - if let Some(ref key) = peerstate.public_key { - public_keyring_for_validate.add_ref(key); + if let Some(key) = peerstate.public_key { + public_keyring_for_validate.add(key); } } out_mail = decrypt_if_autocrypt_message( context, mail, - &private_keyring, - &public_keyring_for_validate, + private_keyring, + public_keyring_for_validate, &mut signatures, - )?; + ) + .await?; } } Ok((out_mail, signatures)) @@ -213,11 +215,11 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail Ok(&mail.subparts[1]) } -fn decrypt_if_autocrypt_message<'a>( +async fn decrypt_if_autocrypt_message<'a>( context: &Context, mail: &ParsedMail<'a>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: Keyring, + public_keyring_for_validate: Keyring, ret_valid_signatures: &mut HashSet, ) -> Result>> { // The returned bool is true if we detected an Autocrypt-encrypted @@ -237,20 +239,19 @@ fn decrypt_if_autocrypt_message<'a>( info!(context, "Detected Autocrypt-mime message"); decrypt_part( - context, encrypted_data_part, private_keyring, public_keyring_for_validate, ret_valid_signatures, ) + .await } /// Returns Ok(None) if nothing encrypted was found. -fn decrypt_part( - _context: &Context, +async fn decrypt_part( mail: &ParsedMail<'_>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: Keyring, + public_keyring_for_validate: Keyring, ret_valid_signatures: &mut HashSet, ) -> Result>> { let data = mail.get_body_raw()?; @@ -260,11 +261,12 @@ fn decrypt_part( ensure!(ret_valid_signatures.is_empty(), "corrupt signatures"); let plain = pgp::pk_decrypt( - &data, - &private_keyring, - &public_keyring_for_validate, + data, + private_keyring, + public_keyring_for_validate, Some(ret_valid_signatures), - )?; + ) + .await?; ensure!(!ret_valid_signatures.is_empty(), "no valid signatures"); return Ok(Some(plain)); @@ -308,14 +310,17 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool { /// If this succeeds you are also guaranteed that the /// [Config::ConfiguredAddr] is configured, this address is returned. // TODO, remove this once deltachat::key::Key no longer exists. -pub fn ensure_secret_key_exists(context: &Context) -> Result { - let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| { - format_err!(concat!( - "Failed to get self address, ", - "cannot ensure secret key if not configured." - )) - })?; - SignedPublicKey::load_self(context)?; +pub async fn ensure_secret_key_exists(context: &Context) -> Result { + let self_addr = context + .get_config(Config::ConfiguredAddr) + .await + .ok_or_else(|| { + format_err!(concat!( + "Failed to get self address, ", + "cannot ensure secret key if not configured." + )) + })?; + SignedPublicKey::load_self(context).await?; Ok(self_addr) } @@ -328,17 +333,17 @@ mod tests { mod ensure_secret_key_exists { use super::*; - #[test] - fn test_prexisting() { - let t = dummy_context(); - let test_addr = configure_alice_keypair(&t.ctx); - assert_eq!(ensure_secret_key_exists(&t.ctx).unwrap(), test_addr); + #[async_std::test] + async fn test_prexisting() { + let t = dummy_context().await; + let test_addr = configure_alice_keypair(&t.ctx).await; + assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr); } - #[test] - fn test_not_configured() { - let t = dummy_context(); - assert!(ensure_secret_key_exists(&t.ctx).is_err()); + #[async_std::test] + async fn test_not_configured() { + let t = dummy_context().await; + assert!(ensure_secret_key_exists(&t.ctx).await.is_err()); } } diff --git a/src/error.rs b/src/error.rs index 20e2782a9..047a0c3c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,13 +2,6 @@ pub use anyhow::{bail, ensure, format_err, Error, Result}; -// #[fail(display = "Invalid Message ID.")] -// InvalidMsgId, -// #[fail(display = "Watch folder not found {:?}", _0)] -// WatchFolderNotFound(String), -// #[fail(display = "Not Configured")] -// NotConfigured, - #[macro_export] macro_rules! ensure_eq { ($left:expr, $right:expr) => ({ diff --git a/src/events.rs b/src/events.rs index dc8a84f67..156e19348 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,12 +1,65 @@ //! # Events specification -use std::path::PathBuf; - +use async_std::path::PathBuf; +use async_std::sync::{channel, Receiver, Sender, TrySendError}; use strum::EnumProperty; use crate::chat::ChatId; use crate::message::MsgId; +#[derive(Debug)] +pub struct Events { + receiver: Receiver, + sender: Sender, +} + +impl Default for Events { + fn default() -> Self { + let (sender, receiver) = channel(1_000); + + Self { receiver, sender } + } +} + +impl Events { + pub fn emit(&self, event: Event) { + match self.sender.try_send(event) { + Ok(()) => {} + Err(TrySendError::Full(event)) => { + // when we are full, we pop remove the oldest event and push on the new one + let _ = self.receiver.try_recv(); + + // try again + self.emit(event); + } + Err(TrySendError::Disconnected(_)) => { + unreachable!("unable to emit event, channel disconnected"); + } + } + } + + /// Retrieve the event emitter. + pub fn get_emitter(&self) -> EventEmitter { + EventEmitter(self.receiver.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct EventEmitter(Receiver); + +impl EventEmitter { + /// Blocking recv of an event. Return `None` if the `Sender` has been droped. + pub fn recv_sync(&self) -> Option { + async_std::task::block_on(self.recv()) + } + + /// Blocking async recv of an event. Return `None` if the `Sender` has been droped. + pub async fn recv(&self) -> Option { + // TODO: change once we can use async channels internally. + self.0.recv().await.ok() + } +} + impl Event { /// Returns the corresponding Event id. pub fn as_id(&self) -> i32 { diff --git a/src/imap/client.rs b/src/imap/client.rs index b2674afa4..e93b273b8 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -1,20 +1,80 @@ +use std::ops::{Deref, DerefMut}; + use async_imap::{ error::{Error as ImapError, Result as ImapResult}, Client as ImapClient, }; -use async_native_tls::TlsStream; use async_std::net::{self, TcpStream}; use super::session::Session; use crate::login_param::{dc_build_tls, CertificateChecks}; +use super::session::SessionStream; + #[derive(Debug)] -pub(crate) enum Client { - Secure(ImapClient>), - Insecure(ImapClient), +pub(crate) struct Client { + is_secure: bool, + inner: ImapClient>, +} + +impl Deref for Client { + type Target = ImapClient>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } impl Client { + pub async fn login, P: AsRef>( + self, + username: U, + password: P, + ) -> std::result::Result { + let Client { inner, is_secure } = self; + let session = inner + .login(username, password) + .await + .map_err(|(err, client)| { + ( + err, + Client { + is_secure, + inner: client, + }, + ) + })?; + Ok(Session { inner: session }) + } + + pub async fn authenticate>( + self, + auth_type: S, + authenticator: &A, + ) -> std::result::Result { + let Client { inner, is_secure } = self; + let session = + inner + .authenticate(auth_type, authenticator) + .await + .map_err(|(err, client)| { + ( + err, + Client { + is_secure, + inner: client, + }, + ) + })?; + Ok(Session { inner: session }) + } + pub async fn connect_secure>( addr: A, domain: S, @@ -22,7 +82,8 @@ impl Client { ) -> ImapResult { let stream = TcpStream::connect(addr).await?; let tls = dc_build_tls(certificate_checks); - let tls_stream = tls.connect(domain.as_ref(), stream).await?; + let tls_stream: Box = + Box::new(tls.connect(domain.as_ref(), stream).await?); let mut client = ImapClient::new(tls_stream); if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() { client.debug = true; @@ -33,11 +94,14 @@ impl Client { .await .ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?; - Ok(Client::Secure(client)) + Ok(Client { + is_secure: true, + inner: client, + }) } pub async fn connect_insecure(addr: A) -> ImapResult { - let stream = TcpStream::connect(addr).await?; + let stream: Box = Box::new(TcpStream::connect(addr).await?); let mut client = ImapClient::new(stream); if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() { @@ -48,7 +112,10 @@ impl Client { .await .ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?; - Ok(Client::Insecure(client)) + Ok(Client { + is_secure: false, + inner: client, + }) } pub async fn secure>( @@ -56,49 +123,21 @@ impl Client { domain: S, certificate_checks: CertificateChecks, ) -> ImapResult { - match self { - Client::Insecure(client) => { - let tls = dc_build_tls(certificate_checks); - let client_sec = client.secure(domain, tls).await?; + if self.is_secure { + Ok(self) + } else { + let Client { mut inner, .. } = self; + let tls = dc_build_tls(certificate_checks); + inner.run_command_and_check_ok("STARTTLS", None).await?; - Ok(Client::Secure(client_sec)) - } - // Nothing to do - Client::Secure(_) => Ok(self), - } - } + let stream = inner.into_inner(); + let ssl_stream = tls.connect(domain.as_ref(), stream).await?; + let boxed: Box = Box::new(ssl_stream); - pub async fn authenticate>( - self, - auth_type: S, - authenticator: &A, - ) -> Result { - match self { - Client::Secure(i) => match i.authenticate(auth_type, authenticator).await { - Ok(session) => Ok(Session::Secure(session)), - Err((err, c)) => Err((err, Client::Secure(c))), - }, - Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await { - Ok(session) => Ok(Session::Insecure(session)), - Err((err, c)) => Err((err, Client::Insecure(c))), - }, - } - } - - pub async fn login, P: AsRef>( - self, - username: U, - password: P, - ) -> Result { - match self { - Client::Secure(i) => match i.login(username, password).await { - Ok(session) => Ok(Session::Secure(session)), - Err((err, c)) => Err((err, Client::Secure(c))), - }, - Client::Insecure(i) => match i.login(username, password).await { - Ok(session) => Ok(Session::Insecure(session)), - Err((err, c)) => Err((err, Client::Insecure(c))), - }, + Ok(Client { + is_secure: true, + inner: ImapClient::new(boxed), + }) } } } diff --git a/src/imap/idle.rs b/src/imap/idle.rs index 30e27b9ee..e6b3ea621 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -1,11 +1,7 @@ use super::Imap; -use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse}; -use async_native_tls::TlsStream; -use async_std::net::TcpStream; +use async_imap::extensions::idle::IdleResponse; use async_std::prelude::*; -use async_std::task; -use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime}; use crate::context::Context; @@ -33,255 +29,199 @@ pub enum Error { SetupHandleError(#[from] super::Error), } -#[derive(Debug)] -pub(crate) enum IdleHandle { - Secure(ImapIdleHandle>), - Insecure(ImapIdleHandle), -} - -impl Session { - pub fn idle(self) -> IdleHandle { - match self { - Session::Secure(i) => { - let h = i.idle(); - IdleHandle::Secure(h) - } - Session::Insecure(i) => { - let h = i.idle(); - IdleHandle::Insecure(h) - } - } - } -} - impl Imap { pub fn can_idle(&self) -> bool { - task::block_on(async move { self.config.read().await.can_idle }) + self.config.can_idle } - pub fn idle(&self, context: &Context, watch_folder: Option) -> Result<()> { - task::block_on(async move { - if !self.can_idle() { - return Err(Error::IdleAbilityMissing); + pub async fn idle(&mut self, context: &Context, watch_folder: Option) -> Result { + use futures::future::FutureExt; + + if !self.can_idle() { + return Err(Error::IdleAbilityMissing); + } + self.setup_handle_if_needed(context).await?; + + self.select_folder(context, watch_folder.clone()).await?; + + let session = self.session.take(); + let timeout = Duration::from_secs(23 * 60); + let mut probe_network = false; + + if let Some(session) = session { + let mut handle = session.idle(); + if let Err(err) = handle.init().await { + return Err(Error::IdleProtocolFailed(err)); } - self.setup_handle_if_needed(context).await?; + let (idle_wait, interrupt) = handle.wait_with_timeout(timeout); - self.select_folder(context, watch_folder.clone()).await?; - - let session = self.session.lock().await.take(); - let timeout = Duration::from_secs(23 * 60); - if let Some(session) = session { - match session.idle() { - // BEWARE: If you change the Secure branch you - // typically also need to change the Insecure branch. - IdleHandle::Secure(mut handle) => { - handle.init().await?; - - let (idle_wait, interrupt) = handle.wait_with_timeout(timeout); - *self.interrupt.lock().await = Some(interrupt); - - if self.skip_next_idle_wait.load(Ordering::SeqCst) { - // interrupt_idle has happened before we - // provided self.interrupt - self.skip_next_idle_wait.store(false, Ordering::SeqCst); - std::mem::drop(idle_wait); - info!(context, "Idle wait was skipped"); - } else { - info!(context, "Idle entering wait-on-remote state"); - match idle_wait.await { - Ok(IdleResponse::NewData(_)) => { - info!(context, "Idle has NewData"); - } - // TODO: idle_wait does not distinguish manual interrupts - // from Timeouts if we would know it's a Timeout we could bail - // directly and reconnect . - Ok(IdleResponse::Timeout) => { - info!(context, "Idle-wait timeout or interruption"); - } - Ok(IdleResponse::ManualInterrupt) => { - info!(context, "Idle wait was interrupted"); - } - Err(err) => { - warn!(context, "Idle wait errored: {:?}", err); - } - } - } - // if we can't properly terminate the idle - // protocol let's break the connection. - let res = - async_std::future::timeout(Duration::from_secs(15), handle.done()) - .await - .map_err(|err| { - self.trigger_reconnect(); - Error::IdleTimeout(err) - })?; - - match res { - Ok(session) => { - *self.session.lock().await = Some(Session::Secure(session)); - } - Err(err) => { - // if we cannot terminate IDLE it probably - // means that we waited long (with idle_wait) - // but the network went away/changed - self.trigger_reconnect(); - return Err(Error::IdleProtocolFailed(err)); - } - } - } - IdleHandle::Insecure(mut handle) => { - handle.init().await?; - - let (idle_wait, interrupt) = handle.wait_with_timeout(timeout); - *self.interrupt.lock().await = Some(interrupt); - - if self.skip_next_idle_wait.load(Ordering::SeqCst) { - // interrupt_idle has happened before we - // provided self.interrupt - self.skip_next_idle_wait.store(false, Ordering::SeqCst); - std::mem::drop(idle_wait); - info!(context, "Idle wait was skipped"); - } else { - info!(context, "Idle entering wait-on-remote state"); - match idle_wait.await { - Ok(IdleResponse::NewData(_)) => { - info!(context, "Idle has NewData"); - } - // TODO: idle_wait does not distinguish manual interrupts - // from Timeouts if we would know it's a Timeout we could bail - // directly and reconnect . - Ok(IdleResponse::Timeout) => { - info!(context, "Idle-wait timeout or interruption"); - } - Ok(IdleResponse::ManualInterrupt) => { - info!(context, "Idle wait was interrupted"); - } - Err(err) => { - warn!(context, "Idle wait errored: {:?}", err); - } - } - } - // if we can't properly terminate the idle - // protocol let's break the connection. - let res = - async_std::future::timeout(Duration::from_secs(15), handle.done()) - .await - .map_err(|err| { - self.trigger_reconnect(); - Error::IdleTimeout(err) - })?; - - match res { - Ok(session) => { - *self.session.lock().await = Some(Session::Insecure(session)); - } - Err(err) => { - // if we cannot terminate IDLE it probably - // means that we waited long (with idle_wait) - // but the network went away/changed - self.trigger_reconnect(); - return Err(Error::IdleProtocolFailed(err)); - } - } - } - } + enum Event { + IdleResponse(IdleResponse), + Interrupt(bool), } - Ok(()) - }) - } - - pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option) { - // Idle using polling. This is also needed if we're not yet configured - - // in this case, we're waiting for a configure job (and an interrupt). - task::block_on(async move { - let fake_idle_start_time = SystemTime::now(); - - info!(context, "IMAP-fake-IDLEing..."); - - let interrupt = stop_token::StopSource::new(); - - // check every minute if there are new messages - // TODO: grow sleep durations / make them more flexible - let interval = async_std::stream::interval(Duration::from_secs(60)); - let mut interrupt_interval = interrupt.stop_token().stop_stream(interval); - *self.interrupt.lock().await = Some(interrupt); - if self.skip_next_idle_wait.load(Ordering::SeqCst) { + if self.skip_next_idle_wait { // interrupt_idle has happened before we // provided self.interrupt - self.skip_next_idle_wait.store(false, Ordering::SeqCst); - info!(context, "fake-idle wait was skipped"); - } else { - // loop until we are interrupted or if we fetched something - while let Some(_) = interrupt_interval.next().await { - // try to connect with proper login params - // (setup_handle_if_needed might not know about them if we - // never successfully connected) - if let Err(err) = self.connect_configured(context) { - warn!(context, "fake_idle: could not connect: {}", err); - continue; - } - if self.config.read().await.can_idle { - // we only fake-idled because network was gone during IDLE, probably - break; - } - info!(context, "fake_idle is connected"); - // we are connected, let's see if fetching messages results - // in anything. If so, we behave as if IDLE had data but - // will have already fetched the messages so perform_*_fetch - // will not find any new. + self.skip_next_idle_wait = false; + drop(idle_wait); + drop(interrupt); - if let Some(ref watch_folder) = watch_folder { - match self.fetch_new_messages(context, watch_folder).await { - Ok(res) => { - info!(context, "fetch_new_messages returned {:?}", res); - if res { - break; - } - } - Err(err) => { - error!(context, "could not fetch from folder: {}", err); - self.trigger_reconnect() - } - } + info!(context, "Idle wait was skipped"); + } else { + info!(context, "Idle entering wait-on-remote state"); + let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race( + self.idle_interrupt.recv().map(|probe_network| { + Ok(Event::Interrupt(probe_network.unwrap_or_default())) + }), + ); + + match fut.await { + Ok(Event::IdleResponse(IdleResponse::NewData(_))) => { + info!(context, "Idle has NewData"); + } + // TODO: idle_wait does not distinguish manual interrupts + // from Timeouts if we would know it's a Timeout we could bail + // directly and reconnect . + Ok(Event::IdleResponse(IdleResponse::Timeout)) => { + info!(context, "Idle-wait timeout or interruption"); + } + Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => { + info!(context, "Idle wait was interrupted"); + } + Ok(Event::Interrupt(probe)) => { + probe_network = probe; + info!(context, "Idle wait was interrupted"); + } + Err(err) => { + warn!(context, "Idle wait errored: {:?}", err); } } } - self.interrupt.lock().await.take(); - info!( - context, - "IMAP-fake-IDLE done after {:.4}s", - SystemTime::now() - .duration_since(fake_idle_start_time) - .unwrap_or_default() - .as_millis() as f64 - / 1000., - ); - }) + // if we can't properly terminate the idle + // protocol let's break the connection. + let res = handle + .done() + .timeout(Duration::from_secs(15)) + .await + .map_err(|err| { + self.trigger_reconnect(); + Error::IdleTimeout(err) + })?; + + match res { + Ok(session) => { + self.session = Some(Session { inner: session }); + } + Err(err) => { + // if we cannot terminate IDLE it probably + // means that we waited long (with idle_wait) + // but the network went away/changed + self.trigger_reconnect(); + return Err(Error::IdleProtocolFailed(err)); + } + } + } + + Ok(probe_network) } - pub fn interrupt_idle(&self, context: &Context) { - task::block_on(async move { - let mut interrupt: Option = self.interrupt.lock().await.take(); - if interrupt.is_none() { - // idle wait is not running, signal it needs to skip - self.skip_next_idle_wait.store(true, Ordering::SeqCst); + pub(crate) async fn fake_idle( + &mut self, + context: &Context, + watch_folder: Option, + ) -> bool { + // Idle using polling. This is also needed if we're not yet configured - + // in this case, we're waiting for a configure job (and an interrupt). - // meanwhile idle-wait may have produced the StopSource - interrupt = self.interrupt.lock().await.take(); + let fake_idle_start_time = SystemTime::now(); + info!(context, "IMAP-fake-IDLEing..."); + + // Do not poll, just wait for an interrupt when no folder is passed in. + if watch_folder.is_none() { + return self.idle_interrupt.recv().await.unwrap_or_default(); + } + + let mut probe_network = false; + if self.skip_next_idle_wait { + // interrupt_idle has happened before we + // provided self.interrupt + self.skip_next_idle_wait = false; + info!(context, "fake-idle wait was skipped"); + } else { + // check every minute if there are new messages + // TODO: grow sleep durations / make them more flexible + let mut interval = async_std::stream::interval(Duration::from_secs(60)); + + enum Event { + Tick, + Interrupt(bool), } - // let's manually drop the StopSource - if interrupt.is_some() { - // the imap thread provided us a stop token but might - // not have entered idle_wait yet, give it some time - // for that to happen. XXX handle this without extra wait - // https://github.com/deltachat/deltachat-core-rust/issues/925 - std::thread::sleep(Duration::from_millis(200)); - info!(context, "low-level: dropping stop-source to interrupt idle"); - std::mem::drop(interrupt) - } - }); + // loop until we are interrupted or if we fetched something + probe_network = + loop { + use futures::future::FutureExt; + match interval + .next() + .map(|_| Event::Tick) + .race(self.idle_interrupt.recv().map(|probe_network| { + Event::Interrupt(probe_network.unwrap_or_default()) + })) + .await + { + Event::Tick => { + // try to connect with proper login params + // (setup_handle_if_needed might not know about them if we + // never successfully connected) + if let Err(err) = self.connect_configured(context).await { + warn!(context, "fake_idle: could not connect: {}", err); + continue; + } + if self.config.can_idle { + // we only fake-idled because network was gone during IDLE, probably + break false; + } + info!(context, "fake_idle is connected"); + // we are connected, let's see if fetching messages results + // in anything. If so, we behave as if IDLE had data but + // will have already fetched the messages so perform_*_fetch + // will not find any new. + + if let Some(ref watch_folder) = watch_folder { + match self.fetch_new_messages(context, watch_folder).await { + Ok(res) => { + info!(context, "fetch_new_messages returned {:?}", res); + if res { + break false; + } + } + Err(err) => { + error!(context, "could not fetch from folder: {}", err); + self.trigger_reconnect() + } + } + } + } + Event::Interrupt(probe_network) => { + // Interrupt + break probe_network; + } + } + }; + } + + info!( + context, + "IMAP-fake-IDLE done after {:.4}s", + SystemTime::now() + .duration_since(fake_idle_start_time) + .unwrap_or_default() + .as_millis() as f64 + / 1000., + ); + + probe_network } } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 6286ae4ff..216689ff6 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -3,16 +3,15 @@ //! uses [async-email/async-imap](https://github.com/async-email/async-imap) //! to implement connect, fetch, delete functionality with standard IMAP servers. -use std::sync::atomic::{AtomicBool, Ordering}; - -use num_traits::FromPrimitive; +use std::collections::BTreeMap; use async_imap::{ error::Result as ImapResult, types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute}, }; -use async_std::sync::{Mutex, RwLock}; -use async_std::task; +use async_std::prelude::*; +use async_std::sync::Receiver; +use num_traits::FromPrimitive; use crate::config::*; use crate::constants::*; @@ -22,7 +21,7 @@ use crate::dc_receive_imf::{ }; use crate::events::Event; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::job::{job_add, Action}; +use crate::job::{self, Action}; use crate::login_param::{CertificateChecks, LoginParam}; use crate::message::{self, update_server_uid}; use crate::mimeparser; @@ -108,14 +107,15 @@ const JUST_UID: &str = "(UID)"; const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])"; const SELECT_ALL: &str = "1:*"; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Imap { - config: RwLock, - session: Mutex>, - connected: Mutex, - interrupt: Mutex>, - skip_next_idle_wait: AtomicBool, - should_reconnect: AtomicBool, + idle_interrupt: Receiver, + config: ImapConfig, + session: Option, + connected: bool, + interrupt: Option, + skip_next_idle_wait: bool, + should_reconnect: bool, } #[derive(Debug)] @@ -181,39 +181,47 @@ impl Default for ImapConfig { } impl Imap { - pub fn new() -> Self { - Default::default() + pub fn new(idle_interrupt: Receiver) -> Self { + Imap { + idle_interrupt, + config: Default::default(), + session: Default::default(), + connected: Default::default(), + interrupt: Default::default(), + skip_next_idle_wait: Default::default(), + should_reconnect: Default::default(), + } } - pub async fn is_connected(&self) -> bool { - *self.connected.lock().await + pub fn is_connected(&self) -> bool { + self.connected } pub fn should_reconnect(&self) -> bool { - self.should_reconnect.load(Ordering::Relaxed) + self.should_reconnect } - pub fn trigger_reconnect(&self) { - self.should_reconnect.store(true, Ordering::Relaxed) + pub fn trigger_reconnect(&mut self) { + self.should_reconnect = true; } - async fn setup_handle_if_needed(&self, context: &Context) -> Result<()> { - if self.config.read().await.imap_server.is_empty() { + async fn setup_handle_if_needed(&mut self, context: &Context) -> Result<()> { + if self.config.imap_server.is_empty() { return Err(Error::InTeardown); } if self.should_reconnect() { self.unsetup_handle(context).await; - self.should_reconnect.store(false, Ordering::Relaxed); - } else if self.is_connected().await { + self.should_reconnect = false; + } else if self.is_connected() { return Ok(()); } - let server_flags = self.config.read().await.server_flags as i32; + let server_flags = self.config.server_flags as i32; let connection_res: ImapResult = if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 { - let config = self.config.read().await; + let config = &mut self.config; let imap_server: &str = config.imap_server.as_ref(); let imap_port = config.imap_port; @@ -228,7 +236,7 @@ impl Imap { Err(err) => Err(err), } } else { - let config = self.config.read().await; + let config = &self.config; let imap_server: &str = config.imap_server.as_ref(); let imap_port = config.imap_port; @@ -242,14 +250,16 @@ impl Imap { let login_res = match connection_res { Ok(client) => { - let config = self.config.read().await; + let config = &self.config; let imap_user: &str = config.imap_user.as_ref(); let imap_pw: &str = config.imap_pw.as_ref(); if (server_flags & DC_LP_AUTH_OAUTH2) != 0 { let addr: &str = config.addr.as_ref(); - if let Some(token) = dc_get_oauth2_access_token(context, addr, imap_pw, true) { + if let Some(token) = + dc_get_oauth2_access_token(context, addr, imap_pw, true).await + { let auth = OAuth2 { user: imap_user.into(), access_token: token, @@ -264,14 +274,16 @@ impl Imap { } Err(err) => { let message = { - let config = self.config.read().await; + let config = &self.config; let imap_server: &str = config.imap_server.as_ref(); let imap_port = config.imap_port; - context.stock_string_repl_str2( - StockMessage::ServerResponse, - format!("IMAP {}:{}", imap_server, imap_port), - err.to_string(), - ) + context + .stock_string_repl_str2( + StockMessage::ServerResponse, + format!("IMAP {}:{}", imap_server, imap_port), + err.to_string(), + ) + .await }; // IMAP connection failures are reported to users emit_event!(context, Event::ErrorNetwork(message)); @@ -279,16 +291,18 @@ impl Imap { } }; - self.should_reconnect.store(false, Ordering::Relaxed); + self.should_reconnect = false; match login_res { Ok(session) => { - *self.session.lock().await = Some(session); + self.session = Some(session); Ok(()) } Err((err, _)) => { - let imap_user = self.config.read().await.imap_user.to_owned(); - let message = context.stock_string_repl_str(StockMessage::CannotLogin, &imap_user); + let imap_user = self.config.imap_user.to_owned(); + let message = context + .stock_string_repl_str(StockMessage::CannotLogin, &imap_user) + .await; emit_event!( context, @@ -300,26 +314,19 @@ impl Imap { } } - async fn unsetup_handle(&self, context: &Context) { - info!( - context, - "IMAP unsetup_handle step 2 (acquiring session.lock)" - ); - if let Some(mut session) = self.session.lock().await.take() { + async fn unsetup_handle(&mut self, context: &Context) { + if let Some(mut session) = self.session.take() { if let Err(err) = session.close().await { warn!(context, "failed to close connection: {:?}", err); } } - *self.connected.lock().await = false; - - info!(context, "IMAP unsetup_handle step 3 (clearing config)."); - self.config.write().await.selected_folder = None; - self.config.write().await.selected_mailbox = None; - info!(context, "IMAP unsetup_handle step 4 (disconnected)"); + self.connected = false; + self.config.selected_folder = None; + self.config.selected_mailbox = None; } - async fn free_connect_params(&self) { - let mut cfg = self.config.write().await; + async fn free_connect_params(&mut self) { + let mut cfg = &mut self.config; cfg.addr = "".into(); cfg.imap_server = "".into(); @@ -332,27 +339,26 @@ impl Imap { } /// Connects to imap account using already-configured parameters. - pub fn connect_configured(&self, context: &Context) -> Result<()> { - if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() { + pub async fn connect_configured(&mut self, context: &Context) -> Result<()> { + if self.is_connected() && !self.should_reconnect() { return Ok(()); } - if !context.sql.get_raw_config_bool(context, "configured") { + if !context.is_configured().await { return Err(Error::ConnectWithoutConfigure); } - let param = LoginParam::from_database(context, "configured_"); + let param = LoginParam::from_database(context, "configured_").await; // the trailing underscore is correct - if task::block_on(self.connect(context, ¶m)) { - self.ensure_configured_folders(context, true) + if self.connect(context, ¶m).await { + self.ensure_configured_folders(context, true).await } else { Err(Error::ConnectionFailed(format!("{}", param))) } } - /// tries connecting to imap account using the specific login - /// parameters - pub async fn connect(&self, context: &Context, lp: &LoginParam) -> bool { + /// Tries connecting to imap account using the specific login parameters. + pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool { if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() { return false; } @@ -365,7 +371,7 @@ impl Imap { let imap_pw = &lp.mail_pw; let server_flags = lp.server_flags as usize; - let mut config = self.config.write().await; + let mut config = &mut self.config; config.addr = addr.to_string(); config.imap_server = imap_server.to_string(); config.imap_port = imap_port; @@ -381,10 +387,10 @@ impl Imap { return false; } - let teardown = match &mut *self.session.lock().await { + let teardown = match &mut self.session { Some(ref mut session) => match session.capabilities().await { Ok(caps) => { - if !context.sql.is_open() { + if !context.sql.is_open().await { warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,); true } else { @@ -398,9 +404,9 @@ impl Imap { } }); - self.config.write().await.can_idle = can_idle; - self.config.write().await.can_move = can_move; - *self.connected.lock().await = true; + self.config.can_idle = can_idle; + self.config.can_move = can_move; + self.connected = true; emit_event!( context, Event::ImapConnected(format!( @@ -420,7 +426,7 @@ impl Imap { }; if teardown { - self.disconnect(context); + self.disconnect(context).await; false } else { @@ -428,15 +434,13 @@ impl Imap { } } - pub fn disconnect(&self, context: &Context) { - task::block_on(async move { - self.unsetup_handle(context).await; - self.free_connect_params().await; - }); + pub async fn disconnect(&mut self, context: &Context) { + self.unsetup_handle(context).await; + self.free_connect_params().await; } - pub async fn fetch(&self, context: &Context, watch_folder: &str) -> Result<()> { - if !context.sql.is_open() { + pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> { + if !context.sql.is_open().await { // probably shutdown return Err(Error::InTeardown); } @@ -448,9 +452,13 @@ impl Imap { Ok(()) } - fn get_config_last_seen_uid>(&self, context: &Context, folder: S) -> (u32, u32) { + async fn get_config_last_seen_uid>( + &self, + context: &Context, + folder: S, + ) -> (u32, u32) { let key = format!("imap.mailbox.{}", folder.as_ref()); - if let Some(entry) = context.sql.get_raw_config(context, &key) { + if let Some(entry) = context.sql.get_raw_config(context, &key).await { // the entry has the format `imap.mailbox.=:` let mut parts = entry.split(':'); ( @@ -471,215 +479,220 @@ impl Imap { } /// return Result with (uid_validity, last_seen_uid) tuple. - pub(crate) fn select_with_uidvalidity( - &self, + pub(crate) async fn select_with_uidvalidity( + &mut self, context: &Context, folder: &str, ) -> Result<(u32, u32)> { - task::block_on(async move { - self.select_folder(context, Some(folder)).await?; + self.select_folder(context, Some(folder)).await?; - // compare last seen UIDVALIDITY against the current one - let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder); + // compare last seen UIDVALIDITY against the current one + let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder).await; - let config = self.config.read().await; - let mailbox = config - .selected_mailbox - .as_ref() - .ok_or_else(|| Error::NoMailbox(folder.to_string()))?; + let config = &mut self.config; + let mailbox = config + .selected_mailbox + .as_ref() + .ok_or_else(|| Error::NoMailbox(folder.to_string()))?; - let new_uid_validity = match mailbox.uid_validity { - Some(v) => v, - None => { - let s = format!("No UIDVALIDITY for folder {:?}", folder); - return Err(Error::Other(s)); - } - }; - - if new_uid_validity == uid_validity { - return Ok((uid_validity, last_seen_uid)); + let new_uid_validity = match mailbox.uid_validity { + Some(v) => v, + None => { + let s = format!("No UIDVALIDITY for folder {:?}", folder); + return Err(Error::Other(s)); } + }; - if mailbox.exists == 0 { - info!(context, "Folder \"{}\" is empty.", folder); + if new_uid_validity == uid_validity { + return Ok((uid_validity, last_seen_uid)); + } - // set lastseenuid=0 for empty folders. - // id we do not do this here, we'll miss the first message - // as we will get in here again and fetch from lastseenuid+1 then + if mailbox.exists == 0 { + info!(context, "Folder \"{}\" is empty.", folder); - self.set_config_last_seen_uid(context, &folder, new_uid_validity, 0); - return Ok((new_uid_validity, 0)); + // set lastseenuid=0 for empty folders. + // id we do not do this here, we'll miss the first message + // as we will get in here again and fetch from lastseenuid+1 then + + self.set_config_last_seen_uid(context, &folder, new_uid_validity, 0) + .await; + return Ok((new_uid_validity, 0)); + } + + // uid_validity has changed or is being set the first time. + // find the last seen uid within the new uid_validity scope. + let new_last_seen_uid = match mailbox.uid_next { + Some(uid_next) => { + uid_next - 1 // XXX could uid_next be 0? } - - // uid_validity has changed or is being set the first time. - // find the last seen uid within the new uid_validity scope. - let new_last_seen_uid = match mailbox.uid_next { - Some(uid_next) => { - uid_next - 1 // XXX could uid_next be 0? - } - None => { - warn!( - context, - "IMAP folder has no uid_next, fall back to fetching" - ); - if let Some(ref mut session) = &mut *self.session.lock().await { - // note that we use fetch by sequence number - // and thus we only need to get exactly the - // last-index message. - let set = format!("{}", mailbox.exists); - match session.fetch(set, JUST_UID).await { - Ok(list) => list[0].uid.unwrap_or_default(), - Err(err) => { - return Err(Error::FetchFailed(err)); + None => { + warn!( + context, + "IMAP folder has no uid_next, fall back to fetching" + ); + if let Some(ref mut session) = &mut self.session { + // note that we use fetch by sequence number + // and thus we only need to get exactly the + // last-index message. + let set = format!("{}", mailbox.exists); + match session.fetch(set, JUST_UID).await { + Ok(mut list) => { + if let Some(Ok(msg)) = list.next().await { + msg.uid.unwrap_or_default() + } else { + return Err(Error::Other("failed to fetch".into())); } } - } else { - return Err(Error::NoConnection); + Err(err) => { + return Err(Error::FetchFailed(err)); + } } + } else { + return Err(Error::NoConnection); } - }; + } + }; - self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid); - info!( - context, - "uid/validity change: new {}/{} current {}/{}", - new_last_seen_uid, - new_uid_validity, - uid_validity, - last_seen_uid - ); - Ok((new_uid_validity, new_last_seen_uid)) - }) + self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid) + .await; + info!( + context, + "uid/validity change: new {}/{} current {}/{}", + new_last_seen_uid, + new_uid_validity, + uid_validity, + last_seen_uid + ); + Ok((new_uid_validity, new_last_seen_uid)) } async fn fetch_new_messages>( - &self, + &mut self, context: &Context, folder: S, ) -> Result { - let show_emails = - ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default(); + let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await) + .unwrap_or_default(); - let (uid_validity, last_seen_uid) = - self.select_with_uidvalidity(context, folder.as_ref())?; + let (uid_validity, last_seen_uid) = self + .select_with_uidvalidity(context, folder.as_ref()) + .await?; - let mut read_cnt = 0; + let msgs = self.fetch_after(context, last_seen_uid).await?; + let read_cnt = msgs.len(); + let folder: &str = folder.as_ref(); - let mut list = if let Some(ref mut session) = &mut *self.session.lock().await { - // fetch messages with larger UID than the last one seen - // `(UID FETCH lastseenuid+1:*)`, see RFC 4549 - let set = format!("{}:*", last_seen_uid + 1); - match session.uid_fetch(set, PREFETCH_FLAGS).await { - Ok(list) => list, - Err(err) => { - return Err(Error::FetchFailed(err)); - } - } - } else { - return Err(Error::NoConnection); - }; - - // prefetch info from all unfetched mails - let mut new_last_seen_uid = last_seen_uid; let mut read_errors = 0; + let mut uids = Vec::with_capacity(msgs.len()); + let mut new_last_seen_uid = None; - list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default()); - - for fetch in &list { - let cur_uid = fetch.uid.unwrap_or_default(); - if cur_uid <= last_seen_uid { - // If the mailbox is not empty, results always include - // at least one UID, even if last_seen_uid+1 is past - // the last UID in the mailbox. It happens because - // uid+1:* is interpreted the same way as *:uid+1. - // See https://tools.ietf.org/html/rfc3501#page-61 for - // standard reference. Therefore, sometimes we receive - // already seen messages and have to filter them out. - info!( - context, - "fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid - ); - continue; - } - read_cnt += 1; - - let headers = get_fetch_headers(fetch)?; - let message_id = prefetch_get_message_id(&headers).unwrap_or_default(); - if let Ok(true) = - precheck_imf(context, &message_id, folder.as_ref(), cur_uid).map_err(|err| { - warn!(context, "precheck_imf error: {}", err); - err - }) - { - // we know the message-id already or don't want the message otherwise. - info!( - context, - "Skipping message {} from \"{}\" by precheck.", - message_id, - folder.as_ref(), - ); - } else { - // we do not know the message-id - // or the message-id is missing (in this case, we create one in the further process) - // or some other error happened - let show = prefetch_should_download(context, &headers, show_emails) - .map_err(|err| { - warn!(context, "prefetch_should_download error: {}", err); - err - }) - .unwrap_or(true); - - if !show { - info!( - context, - "Ignoring new message {} from \"{}\".", - message_id, - folder.as_ref(), - ); - } else { - // check passed, go fetch the rest - if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await { - info!( - context, - "Read error for message {} from \"{}\", trying over later: {}.", - message_id, - folder.as_ref(), - err - ); - read_errors += 1; - } + for (current_uid, msg) in msgs.into_iter() { + let (headers, msg_id) = match get_fetch_headers(&msg) { + Ok(headers) => { + let msg_id = prefetch_get_message_id(&headers).unwrap_or_default(); + (headers, msg_id) } - } - if read_errors == 0 { - new_last_seen_uid = cur_uid; + Err(err) => { + warn!(context, "{}", err); + read_errors += 1; + continue; + } + }; + + if message_needs_processing( + context, + current_uid, + &headers, + &msg_id, + folder, + show_emails, + ) + .await + { + // Trigger download and processing for this message. + uids.push(current_uid); + } else if read_errors == 0 { + // No errors so far, but this was skipped, so mark as last_seen_uid + new_last_seen_uid = Some(current_uid); } } - if new_last_seen_uid > last_seen_uid { - self.set_config_last_seen_uid(context, &folder, uid_validity, new_last_seen_uid); + // check passed, go fetch the emails + let (new_last_seen_uid_processed, error_cnt) = + self.fetch_many_msgs(context, &folder, &uids).await; + read_errors += error_cnt; + + // determine which last_seen_uid to use to update to + let new_last_seen_uid_processed = new_last_seen_uid_processed.unwrap_or_default(); + let new_last_seen_uid = new_last_seen_uid.unwrap_or_default(); + let last_one = new_last_seen_uid.max(new_last_seen_uid_processed); + + if last_one > last_seen_uid { + self.set_config_last_seen_uid(context, &folder, uid_validity, last_one) + .await; } - if read_errors > 0 { + if read_errors == 0 { + info!(context, "{} mails read from \"{}\".", read_cnt, folder,); + } else { warn!( context, - "{} mails read from \"{}\" with {} errors.", - read_cnt, - folder.as_ref(), - read_errors - ); - } else { - info!( - context, - "{} mails read from \"{}\".", - read_cnt, - folder.as_ref() + "{} mails read from \"{}\" with {} errors.", read_cnt, folder, read_errors ); } Ok(read_cnt > 0) } - fn set_config_last_seen_uid>( + /// Fetch all uids larger than the passed in. Returns a sorted list of fetch results. + async fn fetch_after( + &mut self, + context: &Context, + uid: u32, + ) -> Result> { + if self.session.is_none() { + return Err(Error::NoConnection); + } + + let session = self.session.as_mut().unwrap(); + + // fetch messages with larger UID than the last one seen + // `(UID FETCH lastseenuid+1:*)`, see RFC 4549 + let set = format!("{}:*", uid + 1); + let mut list = session + .uid_fetch(set, PREFETCH_FLAGS) + .await + .map_err(Error::FetchFailed)?; + + let mut msgs = BTreeMap::new(); + while let Some(fetch) = list.next().await { + let msg = fetch.map_err(|err| Error::Other(err.to_string()))?; + if let Some(msg_uid) = msg.uid { + msgs.insert(msg_uid, msg); + } + } + drop(list); + + // If the mailbox is not empty, results always include + // at least one UID, even if last_seen_uid+1 is past + // the last UID in the mailbox. It happens because + // uid+1:* is interpreted the same way as *:uid+1. + // See https://tools.ietf.org/html/rfc3501#page-61 for + // standard reference. Therefore, sometimes we receive + // already seen messages and have to filter them out. + let new_msgs = msgs.split_off(&(uid + 1)); + + for current_uid in msgs.keys() { + info!( + context, + "fetch_new_messages: ignoring uid {}, last seen was {}", current_uid, uid + ); + } + + Ok(new_msgs) + } + + async fn set_config_last_seen_uid>( &self, context: &Context, folder: S, @@ -689,174 +702,231 @@ impl Imap { let key = format!("imap.mailbox.{}", folder.as_ref()); let val = format!("{}:{}", uidvalidity, lastseenuid); - context.sql.set_raw_config(context, &key, Some(&val)).ok(); + context + .sql + .set_raw_config(context, &key, Some(&val)) + .await + .ok(); } - /// Fetches a single message by server UID. + /// Fetches a list of messages by server UID. + /// The passed in list of uids must be sorted. /// - /// If it succeeds, the message should be treated as received even - /// if no database entries are created. If the function returns an - /// error, the caller should try again later. - async fn fetch_single_msg>( - &self, + /// Returns the last uid fetch successfully and an error count. + async fn fetch_many_msgs>( + &mut self, context: &Context, folder: S, - server_uid: u32, - ) -> Result<()> { - if !self.is_connected().await { - return Err(Error::Other("Not connected".to_string())); + server_uids: &[u32], + ) -> (Option, usize) { + if server_uids.is_empty() { + return (None, 0); } - let set = format!("{}", server_uid); + if !self.is_connected() { + warn!(context, "Not connected"); + return (None, server_uids.len()); + } - let msgs = if let Some(ref mut session) = &mut *self.session.lock().await { - match session.uid_fetch(set, BODY_FLAGS).await { - Ok(msgs) => msgs, - Err(err) => { - // TODO maybe differentiate between IO and input/parsing problems - // so we don't reconnect if we have a (rare) input/output parsing problem? - self.trigger_reconnect(); - warn!( - context, - "Error on fetching message #{} from folder \"{}\"; error={}.", - server_uid, - folder.as_ref(), - err - ); - return Err(Error::FetchFailed(err)); - } - } - } else { + if self.session.is_none() { // we could not get a valid imap session, this should be retried self.trigger_reconnect(); - return Err(Error::Other("Could not get IMAP session".to_string())); + warn!(context, "Could not get IMAP session"); + return (None, server_uids.len()); + } + + let session = self.session.as_mut().unwrap(); + + let set = if server_uids.len() == 1 { + server_uids[0].to_string() + } else { + let first_uid = server_uids[0]; + let last_uid = server_uids[server_uids.len() - 1]; + assert!(first_uid < last_uid, "uids must be sorted"); + format!("{}:{}", first_uid, last_uid) }; - if let Some(msg) = msgs.first() { - // XXX put flags into a set and pass them to dc_receive_imf - let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted); - let is_seen = msg.flags().any(|flag| flag == Flag::Seen); + let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await { + Ok(msgs) => msgs, + Err(err) => { + // TODO: maybe differentiate between IO and input/parsing problems + // so we don't reconnect if we have a (rare) input/output parsing problem? + self.should_reconnect = true; + warn!( + context, + "Error on fetching messages #{} from folder \"{}\"; error={}.", + &set, + folder.as_ref(), + err + ); + return (None, server_uids.len()); + } + }; - if !is_deleted && msg.body().is_some() { - let body = msg.body().unwrap_or_default(); - if let Err(err) = - dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen) - { - return Err(Error::Other(format!("dc_receive_imf error: {}", err))); + let folder = folder.as_ref().to_string(); + + let mut read_errors = 0; + let mut last_uid = None; + let mut count = 0; + + let mut tasks = Vec::with_capacity(server_uids.len()); + while let Some(Ok(msg)) = msgs.next().await { + let server_uid = msg.uid.unwrap_or_default(); + + if !server_uids.contains(&server_uid) { + // skip if there are some in between we are not interested in + continue; + } + count += 1; + + let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted); + if is_deleted || msg.body().is_none() { + // No need to process these. + continue; + } + + // XXX put flags into a set and pass them to dc_receive_imf + let context = context.clone(); + let folder = folder.clone(); + + let task = async_std::task::spawn(async move { + // safe, as we checked above that there is a body. + let body = msg.body().unwrap(); + let is_seen = msg.flags().any(|flag| flag == Flag::Seen); + + match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await { + Ok(_) => Some(server_uid), + Err(err) => { + warn!(context, "dc_receive_imf error: {}", err); + read_errors += 1; + None + } + } + }); + tasks.push(task); + } + + for task in futures::future::join_all(tasks).await { + match task { + Some(uid) => { + last_uid = Some(uid); + } + None => { + read_errors += 1; } } - } else { + } + + if count != server_uids.len() { warn!( context, - "Message #{} does not exist in folder \"{}\".", - server_uid, - folder.as_ref() + "failed to fetch all uids: got {}, requested {}", + count, + server_uids.len() ); } - Ok(()) + (last_uid, read_errors) } - pub fn can_move(&self) -> bool { - task::block_on(async move { self.config.read().await.can_move }) + pub async fn can_move(&self) -> bool { + self.config.can_move } - pub fn mv( - &self, + pub async fn mv( + &mut self, context: &Context, folder: &str, uid: u32, dest_folder: &str, ) -> ImapActionResult { - task::block_on(async move { - if folder == dest_folder { - info!( - context, - "Skip moving message; message {}/{} is already in {}...", - folder, - uid, - dest_folder, - ); - return ImapActionResult::AlreadyDone; - } - if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) { - return imapresult; - } - // we are connected, and the folder is selected - let set = format!("{}", uid); - let display_folder_id = format!("{}/{}", folder, uid); + if folder == dest_folder { + info!( + context, + "Skip moving message; message {}/{} is already in {}...", folder, uid, dest_folder, + ); + return ImapActionResult::AlreadyDone; + } + if let Some(imapresult) = self + .prepare_imap_operation_on_msg(context, folder, uid) + .await + { + return imapresult; + } + // we are connected, and the folder is selected + let set = format!("{}", uid); + let display_folder_id = format!("{}/{}", folder, uid); - if self.can_move() { - if let Some(ref mut session) = &mut *self.session.lock().await { - match session.uid_mv(&set, &dest_folder).await { - Ok(_) => { - emit_event!( - context, - Event::ImapMessageMoved(format!( - "IMAP Message {} moved to {}", - display_folder_id, dest_folder - )) - ); - return ImapActionResult::Success; - } - Err(err) => { - warn!( - context, - "Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}", - folder, - uid, - dest_folder, - err - ); - } + if self.can_move().await { + if let Some(ref mut session) = &mut self.session { + match session.uid_mv(&set, &dest_folder).await { + Ok(_) => { + emit_event!( + context, + Event::ImapMessageMoved(format!( + "IMAP Message {} moved to {}", + display_folder_id, dest_folder + )) + ); + return ImapActionResult::Success; + } + Err(err) => { + warn!( + context, + "Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}", + folder, + uid, + dest_folder, + err + ); } - } else { - unreachable!(); - }; - } else { - info!( - context, - "Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}", - folder, - uid, - dest_folder - ); - } - - if let Some(ref mut session) = &mut *self.session.lock().await { - if let Err(err) = session.uid_copy(&set, &dest_folder).await { - warn!(context, "Could not copy message: {}", err); - return ImapActionResult::Failed; } } else { unreachable!(); - } + }; + } else { + info!( + context, + "Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}", + folder, + uid, + dest_folder + ); + } - if !self.add_flag_finalized(context, uid, "\\Deleted").await { - warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid); - emit_event!( - context, - Event::ImapMessageMoved(format!( - "IMAP Message {} copied to {} (delete FAILED)", - display_folder_id, dest_folder - )) - ); - ImapActionResult::Failed - } else { - self.config.write().await.selected_folder_needs_expunge = true; - emit_event!( - context, - Event::ImapMessageMoved(format!( - "IMAP Message {} copied to {} (delete successfull)", - display_folder_id, dest_folder - )) - ); - ImapActionResult::Success + if let Some(ref mut session) = &mut self.session { + if let Err(err) = session.uid_copy(&set, &dest_folder).await { + warn!(context, "Could not copy message: {}", err); + return ImapActionResult::Failed; } - }) + } else { + unreachable!(); + } + + if !self.add_flag_finalized(context, uid, "\\Deleted").await { + warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid); + emit_event!( + context, + Event::ImapMessageMoved(format!( + "IMAP Message {} copied to {} (delete FAILED)", + display_folder_id, dest_folder + )) + ); + ImapActionResult::Failed + } else { + self.config.selected_folder_needs_expunge = true; + emit_event!( + context, + Event::ImapMessageMoved(format!( + "IMAP Message {} copied to {} (delete successfull)", + display_folder_id, dest_folder + )) + ); + ImapActionResult::Success + } } - async fn add_flag_finalized(&self, context: &Context, server_uid: u32, flag: &str) -> bool { + async fn add_flag_finalized(&mut self, context: &Context, server_uid: u32, flag: &str) -> bool { // return true if we successfully set the flag or we otherwise // think add_flag should not be retried: Disconnection during setting // the flag, or other imap-errors, returns true as well. @@ -870,7 +940,7 @@ impl Imap { } async fn add_flag_finalized_with_set( - &self, + &mut self, context: &Context, uid_set: &str, flag: &str, @@ -878,7 +948,7 @@ impl Imap { if self.should_reconnect() { return false; } - if let Some(ref mut session) = &mut *self.session.lock().await { + if let Some(ref mut session) = &mut self.session { let query = format!("+FLAGS ({})", flag); match session.uid_store(uid_set, &query).await { Ok(_) => {} @@ -895,338 +965,342 @@ impl Imap { } } - pub fn prepare_imap_operation_on_msg( - &self, + pub async fn prepare_imap_operation_on_msg( + &mut self, context: &Context, folder: &str, uid: u32, ) -> Option { - task::block_on(async move { - if uid == 0 { - return Some(ImapActionResult::Failed); + if uid == 0 { + return Some(ImapActionResult::Failed); + } + if !self.is_connected() { + // currently jobs are only performed on the INBOX thread + // TODO: make INBOX/SENT/MVBOX perform the jobs on their + // respective folders to avoid select_folder network traffic + // and the involved error states + if let Err(err) = self.connect_configured(context).await { + warn!(context, "prepare_imap_op failed: {}", err); + return Some(ImapActionResult::RetryLater); } - if !self.is_connected().await { - // currently jobs are only performed on the INBOX thread - // TODO: make INBOX/SENT/MVBOX perform the jobs on their - // respective folders to avoid select_folder network traffic - // and the involved error states - if let Err(err) = self.connect_configured(context) { - warn!(context, "prepare_imap_op failed: {}", err); - return Some(ImapActionResult::RetryLater); - } + } + match self.select_folder(context, Some(&folder)).await { + Ok(()) => None, + Err(select_folder::Error::ConnectionLost) => { + warn!(context, "Lost imap connection"); + Some(ImapActionResult::RetryLater) } - match self.select_folder(context, Some(&folder)).await { - Ok(()) => None, - Err(select_folder::Error::ConnectionLost) => { - warn!(context, "Lost imap connection"); - Some(ImapActionResult::RetryLater) - } - Err(select_folder::Error::NoSession) => { - warn!(context, "no imap session"); - Some(ImapActionResult::Failed) - } - Err(select_folder::Error::BadFolderName(folder_name)) => { - warn!(context, "invalid folder name: {:?}", folder_name); - Some(ImapActionResult::Failed) - } - Err(err) => { - warn!(context, "failed to select folder: {:?}: {:?}", folder, err); - Some(ImapActionResult::RetryLater) - } + Err(select_folder::Error::NoSession) => { + warn!(context, "no imap session"); + Some(ImapActionResult::Failed) } - }) + Err(select_folder::Error::BadFolderName(folder_name)) => { + warn!(context, "invalid folder name: {:?}", folder_name); + Some(ImapActionResult::Failed) + } + Err(err) => { + warn!(context, "failed to select folder: {:?}: {:?}", folder, err); + Some(ImapActionResult::RetryLater) + } + } } - pub fn set_seen(&self, context: &Context, folder: &str, uid: u32) -> ImapActionResult { - task::block_on(async move { - if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) { - return imapresult; - } - // we are connected, and the folder is selected - info!(context, "Marking message {}/{} as seen...", folder, uid,); + pub async fn set_seen( + &mut self, + context: &Context, + folder: &str, + uid: u32, + ) -> ImapActionResult { + if let Some(imapresult) = self + .prepare_imap_operation_on_msg(context, folder, uid) + .await + { + return imapresult; + } + // we are connected, and the folder is selected + info!(context, "Marking message {}/{} as seen...", folder, uid,); - if self.add_flag_finalized(context, uid, "\\Seen").await { - ImapActionResult::Success - } else { - warn!( - context, - "Cannot mark message {} in folder {} as seen, ignoring.", uid, folder - ); - ImapActionResult::Failed - } - }) + if self.add_flag_finalized(context, uid, "\\Seen").await { + ImapActionResult::Success + } else { + warn!( + context, + "Cannot mark message {} in folder {} as seen, ignoring.", uid, folder + ); + ImapActionResult::Failed + } } - pub fn delete_msg( - &self, + pub async fn delete_msg( + &mut self, context: &Context, message_id: &str, folder: &str, uid: u32, ) -> ImapActionResult { - task::block_on(async move { - if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) { - return imapresult; - } - // we are connected, and the folder is selected + if let Some(imapresult) = self + .prepare_imap_operation_on_msg(context, folder, uid) + .await + { + return imapresult; + } + // we are connected, and the folder is selected - let set = format!("{}", uid); - let display_imap_id = format!("{}/{}", folder, uid); + let set = format!("{}", uid); + let display_imap_id = format!("{}/{}", folder, uid); - // double-check that we are deleting the correct message-id - // this comes at the expense of another imap query - if let Some(ref mut session) = &mut *self.session.lock().await { - match session.uid_fetch(set, DELETE_CHECK_FLAGS).await { - Ok(msgs) => { - let fetch = if let Some(fetch) = msgs.first() { - fetch - } else { - warn!( - context, - "Cannot delete on IMAP, {}: imap entry gone '{}'", - display_imap_id, - message_id, - ); - return ImapActionResult::AlreadyDone; - }; - - let remote_message_id = get_fetch_headers(fetch) - .and_then(|headers| prefetch_get_message_id(&headers)) - .unwrap_or_default(); - - if remote_message_id != message_id { - warn!( - context, - "Cannot delete on IMAP, {}: remote message-id '{}' != '{}'", - display_imap_id, - remote_message_id, - message_id, - ); - return ImapActionResult::Failed; - } - } - Err(err) => { + // double-check that we are deleting the correct message-id + // this comes at the expense of another imap query + if let Some(ref mut session) = &mut self.session { + match session.uid_fetch(set, DELETE_CHECK_FLAGS).await { + Ok(mut msgs) => { + let fetch = if let Some(Ok(fetch)) = msgs.next().await { + fetch + } else { warn!( context, - "Cannot delete {} on IMAP: {}", display_imap_id, err + "Cannot delete on IMAP, {}: imap entry gone '{}'", + display_imap_id, + message_id, ); - return ImapActionResult::RetryLater; + return ImapActionResult::AlreadyDone; + }; + + let remote_message_id = get_fetch_headers(&fetch) + .and_then(|headers| prefetch_get_message_id(&headers)) + .unwrap_or_default(); + + if remote_message_id != message_id { + warn!( + context, + "Cannot delete on IMAP, {}: remote message-id '{}' != '{}'", + display_imap_id, + remote_message_id, + message_id, + ); + return ImapActionResult::Failed; } } + Err(err) => { + warn!( + context, + "Cannot delete on IMAP, {}: {}", display_imap_id, err, + ); + return ImapActionResult::RetryLater; + } } + } - // mark the message for deletion - if !self.add_flag_finalized(context, uid, "\\Deleted").await { - warn!( - context, - "Cannot mark message {} as \"Deleted\".", display_imap_id - ); - ImapActionResult::RetryLater - } else { - emit_event!( - context, - Event::ImapMessageDeleted(format!( - "IMAP Message {} marked as deleted [{}]", - display_imap_id, message_id - )) - ); - self.config.write().await.selected_folder_needs_expunge = true; - ImapActionResult::Success - } - }) + // mark the message for deletion + if !self.add_flag_finalized(context, uid, "\\Deleted").await { + warn!( + context, + "Cannot mark message {} as \"Deleted\".", display_imap_id + ); + ImapActionResult::RetryLater + } else { + emit_event!( + context, + Event::ImapMessageDeleted(format!( + "IMAP Message {} marked as deleted [{}]", + display_imap_id, message_id + )) + ); + self.config.selected_folder_needs_expunge = true; + ImapActionResult::Success + } } - pub fn ensure_configured_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> { + pub async fn ensure_configured_folders( + &mut self, + context: &Context, + create_mvbox: bool, + ) -> Result<()> { let folders_configured = context .sql - .get_raw_config_int(context, "folders_configured"); + .get_raw_config_int(context, "folders_configured") + .await; if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION { return Ok(()); } - self.configure_folders(context, create_mvbox) + self.configure_folders(context, create_mvbox).await } - pub fn configure_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> { - task::block_on(async move { - if !self.is_connected().await { - return Err(Error::NoConnection); - } + pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> { + if !self.is_connected() { + return Err(Error::NoConnection); + } - info!(context, "Configuring IMAP-folders."); + if let Some(ref mut session) = &mut self.session { + let mut folders = match session.list(Some(""), Some("*")).await { + Ok(f) => f, + Err(err) => { + return Err(Error::Other(format!("list_folders failed {:?}", err))); + } + }; - if let Some(ref mut session) = &mut *self.session.lock().await { - let folders = match self.list_folders(session, context).await { - Some(f) => f, - None => { - return Err(Error::Other("list_folders failed".to_string())); - } - }; + let mut sentbox_folder = None; + let mut mvbox_folder = None; - let sentbox_folder = folders - .iter() - .find(|folder| get_folder_meaning(folder) == FolderMeaning::SentObjects) - .or_else(|| { - info!(context, "can't find sentbox by attributes, checking names"); - folders.iter().find(|folder| { - get_folder_meaning_by_name(folder) == FolderMeaning::SentObjects - }) - }); - info!(context, "sentbox folder is {:?}", sentbox_folder); - - let mut delimiter = "."; - if let Some(folder) = folders.first() { - if let Some(d) = folder.delimiter() { - if !d.is_empty() { - delimiter = d; - } + let mut delimiter = ".".to_string(); + if let Some(folder) = folders.next().await { + let folder = folder.map_err(|err| Error::Other(err.to_string()))?; + if let Some(d) = folder.delimiter() { + if !d.is_empty() { + delimiter = d.to_string(); } } - info!(context, "Using \"{}\" as folder-delimiter.", delimiter); - let fallback_folder = format!("INBOX{}DeltaChat", delimiter); + } + info!(context, "Using \"{}\" as folder-delimiter.", delimiter); + let fallback_folder = format!("INBOX{}DeltaChat", delimiter); - let mut mvbox_folder = folders - .iter() - .find(|folder| folder.name() == "DeltaChat" || folder.name() == fallback_folder) - .map(|n| n.name().to_string()); + while let Some(folder) = folders.next().await { + let folder = folder.map_err(|err| Error::Other(err.to_string()))?; + info!(context, "Scanning folder: {:?}", folder); - if mvbox_folder.is_none() && create_mvbox { - info!(context, "Creating MVBOX-folder \"DeltaChat\"...",); + if mvbox_folder.is_none() + && (folder.name() == "DeltaChat" || folder.name() == fallback_folder) + { + mvbox_folder = Some(folder.name().to_string()); + } - match session.create("DeltaChat").await { - Ok(_) => { - mvbox_folder = Some("DeltaChat".into()); + if sentbox_folder.is_none() { + if let FolderMeaning::SentObjects = get_folder_meaning(&folder) { + sentbox_folder = Some(folder); + } else if let FolderMeaning::SentObjects = get_folder_meaning_by_name(&folder) { + sentbox_folder = Some(folder); + } + } - info!(context, "MVBOX-folder created.",); - } - Err(err) => { - warn!( - context, - "Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})", - err - ); + if mvbox_folder.is_some() && sentbox_folder.is_some() { + break; + } + } + info!(context, "sentbox folder is {:?}", sentbox_folder); - match session.create(&fallback_folder).await { - Ok(_) => { - mvbox_folder = Some(fallback_folder); - info!( - context, - "MVBOX-folder created as INBOX subfolder. ({})", err - ); - } - Err(err) => { - warn!(context, "Cannot create MVBOX-folder. ({})", err); - } + drop(folders); + + if mvbox_folder.is_none() && create_mvbox { + info!(context, "Creating MVBOX-folder \"DeltaChat\"...",); + + match session.create("DeltaChat").await { + Ok(_) => { + mvbox_folder = Some("DeltaChat".into()); + + info!(context, "MVBOX-folder created.",); + } + Err(err) => { + warn!( + context, + "Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})", + err + ); + + match session.create(&fallback_folder).await { + Ok(_) => { + mvbox_folder = Some(fallback_folder); + info!( + context, + "MVBOX-folder created as INBOX subfolder. ({})", err + ); + } + Err(err) => { + warn!(context, "Cannot create MVBOX-folder. ({})", err); } } } - // SUBSCRIBE is needed to make the folder visible to the LSUB command - // that may be used by other MUAs to list folders. - // for the LIST command, the folder is always visible. - if let Some(ref mvbox) = mvbox_folder { - if let Err(err) = session.subscribe(mvbox).await { - warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err); - } + } + // SUBSCRIBE is needed to make the folder visible to the LSUB command + // that may be used by other MUAs to list folders. + // for the LIST command, the folder is always visible. + if let Some(ref mvbox) = mvbox_folder { + if let Err(err) = session.subscribe(mvbox).await { + warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err); } } + } + context + .sql + .set_raw_config(context, "configured_inbox_folder", Some("INBOX")) + .await?; + if let Some(ref mvbox_folder) = mvbox_folder { context .sql - .set_raw_config(context, "configured_inbox_folder", Some("INBOX"))?; - if let Some(ref mvbox_folder) = mvbox_folder { - context.sql.set_raw_config( - context, - "configured_mvbox_folder", - Some(mvbox_folder), - )?; - } - if let Some(ref sentbox_folder) = sentbox_folder { - context.sql.set_raw_config( + .set_raw_config(context, "configured_mvbox_folder", Some(mvbox_folder)) + .await?; + } + if let Some(ref sentbox_folder) = sentbox_folder { + context + .sql + .set_raw_config( context, "configured_sentbox_folder", Some(sentbox_folder.name()), - )?; - } - context.sql.set_raw_config_int( - context, - "folders_configured", - DC_FOLDERS_CONFIGURED_VERSION, - )?; + ) + .await?; } - info!(context, "FINISHED configuring IMAP-folders."); - Ok(()) - }) + context + .sql + .set_raw_config_int(context, "folders_configured", DC_FOLDERS_CONFIGURED_VERSION) + .await?; + } + info!(context, "FINISHED configuring IMAP-folders."); + Ok(()) } - async fn list_folders(&self, session: &mut Session, context: &Context) -> Option> { - match session.list(Some(""), Some("*")).await { - Ok(list) => { - if list.is_empty() { - warn!(context, "Folder list is empty.",); - } - Some(list) + pub async fn empty_folder(&mut self, context: &Context, folder: &str) { + info!(context, "emptying folder {}", folder); + + // we want to report all error to the user + // (no retry should be attempted) + if folder.is_empty() { + error!(context, "cannot perform empty, folder not set"); + return; + } + if let Err(err) = self.setup_handle_if_needed(context).await { + error!(context, "could not setup imap connection: {:?}", err); + return; + } + if let Err(err) = self.select_folder(context, Some(&folder)).await { + error!( + context, + "Could not select {} for expunging: {:?}", folder, err + ); + return; + } + + if !self + .add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted") + .await + { + error!(context, "Cannot mark messages for deletion {}", folder); + return; + } + + // we now trigger expunge to actually delete messages + self.config.selected_folder_needs_expunge = true; + match self.select_folder::(context, None).await { + Ok(()) => { + emit_event!(context, Event::ImapFolderEmptied(folder.to_string())); } Err(err) => { - eprintln!("list error: {:?}", err); - warn!(context, "Cannot get folder list.",); - - None + error!(context, "expunge failed {}: {:?}", folder, err); } } - } - - pub fn empty_folder(&self, context: &Context, folder: &str) { - task::block_on(async move { - info!(context, "emptying folder {}", folder); - - // we want to report all error to the user - // (no retry should be attempted) - if folder.is_empty() { - error!(context, "cannot perform empty, folder not set"); - return; - } - if let Err(err) = self.setup_handle_if_needed(context).await { - error!(context, "could not setup imap connection: {}", err); - return; - } - if let Err(err) = self.select_folder(context, Some(&folder)).await { - error!( - context, - "Could not select {} for expunging: {}", folder, err - ); - return; - } - - if !self - .add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted") - .await - { - error!(context, "Cannot mark messages for deletion {}", folder); - return; - } - - // we now trigger expunge to actually delete messages - self.config.write().await.selected_folder_needs_expunge = true; - match self.select_folder::(context, None).await { - Ok(()) => { - emit_event!(context, Event::ImapFolderEmptied(folder.to_string())); - } - Err(err) => { - error!(context, "expunge failed {}: {}", folder, err); - } - } - - if let Err(err) = crate::sql::execute( - context, - &context.sql, + if let Err(err) = context + .sql + .execute( "UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?", - params![folder], - ) { - warn!( - context, - "Failed to reset server_uid and server_folder for deleted messages: {}", err - ); - } - }); + paramsv![folder], + ) + .await + { + warn!( + context, + "Failed to reset server_uid and server_folder for deleted messages: {}", err + ); + } } } @@ -1262,14 +1336,14 @@ fn get_folder_meaning(folder_name: &Name) -> FolderMeaning { FolderMeaning::Unknown } -fn precheck_imf( +async fn precheck_imf( context: &Context, rfc724_mid: &str, server_folder: &str, server_uid: u32, ) -> Result { if let Some((old_server_folder, old_server_uid, msg_id)) = - message::rfc724_mid_exists(context, &rfc724_mid)? + message::rfc724_mid_exists(context, &rfc724_mid).await? { if old_server_folder.is_empty() && old_server_uid == 0 { info!( @@ -1277,17 +1351,17 @@ fn precheck_imf( "[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid ); - let delete_server_after = context.get_config_delete_server_after(); + let delete_server_after = context.get_config_delete_server_after().await; if delete_server_after != Some(0) { - context.do_heuristics_moves(server_folder.as_ref(), msg_id); - job_add( + context + .do_heuristics_moves(server_folder.as_ref(), msg_id) + .await; + job::add( context, - Action::MarkseenMsgOnImap, - msg_id.to_u32() as i32, - Params::new(), - 0, - ); + job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0), + ) + .await; } } else if old_server_folder != server_folder { info!( @@ -1321,7 +1395,7 @@ fn precheck_imf( } if old_server_folder != server_folder || old_server_uid != server_uid { - update_server_uid(context, &rfc724_mid, server_folder, server_uid); + update_server_uid(context, &rfc724_mid, server_folder, server_uid).await; } Ok(true) } else { @@ -1346,15 +1420,18 @@ fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result } } -fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::MailHeader]) -> bool { +async fn prefetch_is_reply_to_chat_message( + context: &Context, + headers: &[mailparse::MailHeader<'_>], +) -> bool { if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo) { - if is_msgrmsg_rfc724_mid_in_list(context, &value) { + if is_msgrmsg_rfc724_mid_in_list(context, &value).await { return true; } } if let Some(value) = headers.get_header_value(HeaderDef::References) { - if is_msgrmsg_rfc724_mid_in_list(context, &value) { + if is_msgrmsg_rfc724_mid_in_list(context, &value).await { return true; } } @@ -1362,13 +1439,13 @@ fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::Ma false } -fn prefetch_should_download( +async fn prefetch_should_download( context: &Context, - headers: &[mailparse::MailHeader], + headers: &[mailparse::MailHeader<'_>], show_emails: ShowEmails, ) -> Result { let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some(); - let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers); + let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers).await; // Autocrypt Setup Message should be shown even if it is from non-chat client. let is_autocrypt_setup_message = headers @@ -1376,7 +1453,7 @@ fn prefetch_should_download( .is_some(); let (_contact_id, blocked_contact, origin) = - from_field_to_contact_id(context, &mimeparser::get_from(headers))?; + from_field_to_contact_id(context, &mimeparser::get_from(headers)).await?; let accepted_contact = origin.is_known(); let show = is_autocrypt_setup_message @@ -1390,3 +1467,50 @@ fn prefetch_should_download( let show = show && !blocked_contact; Ok(show) } + +async fn message_needs_processing( + context: &Context, + current_uid: u32, + headers: &[mailparse::MailHeader<'_>], + msg_id: &str, + folder: &str, + show_emails: ShowEmails, +) -> bool { + let skip = match precheck_imf(context, &msg_id, folder, current_uid).await { + Ok(skip) => skip, + Err(err) => { + warn!(context, "precheck_imf error: {}", err); + true + } + }; + + if skip { + // we know the message-id already or don't want the message otherwise. + info!( + context, + "Skipping message {} from \"{}\" by precheck.", msg_id, folder, + ); + return false; + } + + // we do not know the message-id + // or the message-id is missing (in this case, we create one in the further process) + // or some other error happened + let show = match prefetch_should_download(context, &headers, show_emails).await { + Ok(show) => show, + Err(err) => { + warn!(context, "prefetch_should_download error: {}", err); + true + } + }; + + if !show { + info!( + context, + "Ignoring new message {} from \"{}\".", msg_id, folder, + ); + return false; + } + + true +} diff --git a/src/imap/select_folder.rs b/src/imap/select_folder.rs index 4d454c332..35e2b4eb4 100644 --- a/src/imap/select_folder.rs +++ b/src/imap/select_folder.rs @@ -27,11 +27,11 @@ impl Imap { /// /// CLOSE is considerably faster than an EXPUNGE, see /// https://tools.ietf.org/html/rfc3501#section-6.4.2 - async fn close_folder(&self, context: &Context) -> Result<()> { - if let Some(ref folder) = self.config.read().await.selected_folder { + async fn close_folder(&mut self, context: &Context) -> Result<()> { + if let Some(ref folder) = self.config.selected_folder { info!(context, "Expunge messages in \"{}\".", folder); - if let Some(ref mut session) = &mut *self.session.lock().await { + if let Some(ref mut session) = self.session { match session.close().await { Ok(_) => { info!(context, "close/expunge succeeded"); @@ -45,40 +45,45 @@ impl Imap { return Err(Error::NoSession); } } - let mut cfg = self.config.write().await; - cfg.selected_folder = None; - cfg.selected_folder_needs_expunge = false; + self.config.selected_folder = None; + self.config.selected_folder_needs_expunge = false; + Ok(()) } /// select a folder, possibly update uid_validity and, if needed, /// expunge the folder to remove delete-marked messages. pub(super) async fn select_folder>( - &self, + &mut self, context: &Context, folder: Option, ) -> Result<()> { - if self.session.lock().await.is_none() { - let mut cfg = self.config.write().await; - cfg.selected_folder = None; - cfg.selected_folder_needs_expunge = false; + if self.session.is_none() { + self.config.selected_folder = None; + self.config.selected_folder_needs_expunge = false; self.trigger_reconnect(); return Err(Error::NoSession); } - let needs_expunge = self.config.read().await.selected_folder_needs_expunge; + // if there is a new folder and the new folder is equal to the selected one, there's nothing to do. + // if there is _no_ new folder, we continue as we might want to expunge below. + if let Some(ref folder) = folder { + if let Some(ref selected_folder) = self.config.selected_folder { + if folder.as_ref() == selected_folder { + return Ok(()); + } + } + } + + // deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then) + let needs_expunge = { self.config.selected_folder_needs_expunge }; if needs_expunge { self.close_folder(context).await?; } - let folder_str: Option<&str> = folder.as_ref().map(|x| x.as_ref()); - if self.config.read().await.selected_folder.as_deref() == folder_str { - return Ok(()); - } - // select new folder if let Some(ref folder) = folder { - if let Some(ref mut session) = &mut *self.session.lock().await { + if let Some(ref mut session) = &mut self.session { let res = session.select(folder).await; // https://tools.ietf.org/html/rfc3501#section-6.3.1 @@ -87,21 +92,20 @@ impl Imap { match res { Ok(mailbox) => { - let mut config = self.config.write().await; - config.selected_folder = Some(folder.as_ref().to_string()); - config.selected_mailbox = Some(mailbox); + self.config.selected_folder = Some(folder.as_ref().to_string()); + self.config.selected_mailbox = Some(mailbox); Ok(()) } Err(async_imap::error::Error::ConnectionLost) => { self.trigger_reconnect(); - self.config.write().await.selected_folder = None; + self.config.selected_folder = None; Err(Error::ConnectionLost) } Err(async_imap::error::Error::Validate(_)) => { Err(Error::BadFolderName(folder.as_ref().to_string())) } Err(err) => { - self.config.write().await.selected_folder = None; + self.config.selected_folder = None; self.trigger_reconnect(); Err(Error::Other(err.to_string())) } diff --git a/src/imap/session.rs b/src/imap/session.rs index c4f9d0a95..e533cc22e 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,172 +1,40 @@ -use async_imap::{ - error::Result as ImapResult, - types::{Capabilities, Fetch, Mailbox, Name}, - Session as ImapSession, -}; +use std::ops::{Deref, DerefMut}; + +use async_imap::Session as ImapSession; use async_native_tls::TlsStream; use async_std::net::TcpStream; -use async_std::prelude::*; #[derive(Debug)] -pub(crate) enum Session { - Secure(ImapSession>), - Insecure(ImapSession), +pub(crate) struct Session { + pub(super) inner: ImapSession>, +} + +pub(crate) trait SessionStream: + async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug +{ +} + +impl SessionStream for TlsStream> {} +impl SessionStream for TlsStream {} +impl SessionStream for TcpStream {} + +impl Deref for Session { + type Target = ImapSession>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Session { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } impl Session { - pub async fn capabilities(&mut self) -> ImapResult { - let res = match self { - Session::Secure(i) => i.capabilities().await?, - Session::Insecure(i) => i.capabilities().await?, - }; - - Ok(res) - } - - pub async fn list( - &mut self, - reference_name: Option<&str>, - mailbox_pattern: Option<&str>, - ) -> ImapResult> { - let res = match self { - Session::Secure(i) => { - i.list(reference_name, mailbox_pattern) - .await? - .collect::>() - .await? - } - Session::Insecure(i) => { - i.list(reference_name, mailbox_pattern) - .await? - .collect::>() - .await? - } - }; - Ok(res) - } - - pub async fn create>(&mut self, mailbox_name: S) -> ImapResult<()> { - match self { - Session::Secure(i) => i.create(mailbox_name).await?, - Session::Insecure(i) => i.create(mailbox_name).await?, - } - Ok(()) - } - - pub async fn subscribe>(&mut self, mailbox: S) -> ImapResult<()> { - match self { - Session::Secure(i) => i.subscribe(mailbox).await?, - Session::Insecure(i) => i.subscribe(mailbox).await?, - } - Ok(()) - } - - pub async fn close(&mut self) -> ImapResult<()> { - match self { - Session::Secure(i) => i.close().await?, - Session::Insecure(i) => i.close().await?, - } - Ok(()) - } - - pub async fn select>(&mut self, mailbox_name: S) -> ImapResult { - let mbox = match self { - Session::Secure(i) => i.select(mailbox_name).await?, - Session::Insecure(i) => i.select(mailbox_name).await?, - }; - - Ok(mbox) - } - - pub async fn fetch(&mut self, sequence_set: S1, query: S2) -> ImapResult> - where - S1: AsRef, - S2: AsRef, - { - let res = match self { - Session::Secure(i) => { - i.fetch(sequence_set, query) - .await? - .collect::>() - .await? - } - Session::Insecure(i) => { - i.fetch(sequence_set, query) - .await? - .collect::>() - .await? - } - }; - Ok(res) - } - - pub async fn uid_fetch(&mut self, uid_set: S1, query: S2) -> ImapResult> - where - S1: AsRef, - S2: AsRef, - { - let res = match self { - Session::Secure(i) => { - i.uid_fetch(uid_set, query) - .await? - .collect::>() - .await? - } - Session::Insecure(i) => { - i.uid_fetch(uid_set, query) - .await? - .collect::>() - .await? - } - }; - - Ok(res) - } - - pub async fn uid_store(&mut self, uid_set: S1, query: S2) -> ImapResult> - where - S1: AsRef, - S2: AsRef, - { - let res = match self { - Session::Secure(i) => { - i.uid_store(uid_set, query) - .await? - .collect::>() - .await? - } - Session::Insecure(i) => { - i.uid_store(uid_set, query) - .await? - .collect::>() - .await? - } - }; - Ok(res) - } - - pub async fn uid_mv, S2: AsRef>( - &mut self, - uid_set: S1, - mailbox_name: S2, - ) -> ImapResult<()> { - match self { - Session::Secure(i) => i.uid_mv(uid_set, mailbox_name).await?, - Session::Insecure(i) => i.uid_mv(uid_set, mailbox_name).await?, - } - Ok(()) - } - - pub async fn uid_copy, S2: AsRef>( - &mut self, - uid_set: S1, - mailbox_name: S2, - ) -> ImapResult<()> { - match self { - Session::Secure(i) => i.uid_copy(uid_set, mailbox_name).await?, - Session::Insecure(i) => i.uid_copy(uid_set, mailbox_name).await?, - } - - Ok(()) + pub fn idle(self) -> async_imap::extensions::idle::Handle> { + let Session { inner } = self; + inner.idle() } } diff --git a/src/imex.rs b/src/imex.rs index 849705d04..563b6aa68 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -1,9 +1,9 @@ //! # Import/export module -use core::cmp::{max, min}; -use std::path::Path; +use std::cmp::{max, min}; -use num_traits::FromPrimitive; +use async_std::path::{Path, PathBuf}; +use async_std::prelude::*; use rand::{thread_rng, Rng}; use crate::blob::BlobObject; @@ -16,7 +16,6 @@ use crate::dc_tools::*; use crate::e2ee; use crate::error::*; use crate::events::Event; -use crate::job::*; use crate::key::{self, DcKey, Key, SignedSecretKey}; use crate::message::{Message, MsgId}; use crate::mimeparser::SystemMessage; @@ -53,13 +52,10 @@ pub enum ImexMode { } /// Import/export things. -/// For this purpose, the function creates a job that is executed in the IMAP-thread then; -/// this requires to call dc_perform_inbox_jobs() regularly. /// /// What to do is defined by the *what* parameter. /// -/// While dc_imex() returns immediately, the started job may take a while, -/// you can stop it using dc_stop_ongoing_process(). During execution of the job, +/// During execution of the job, /// some events are sent out: /// /// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create @@ -68,41 +64,48 @@ pub enum ImexMode { /// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN /// /// Only one import-/export-progress can run at the same time. -/// To cancel an import-/export-progress, use dc_stop_ongoing_process(). -pub fn imex(context: &Context, what: ImexMode, param1: Option>) { - let mut param = Params::new(); - param.set_int(Param::Cmd, what as i32); - if let Some(param1) = param1 { - param.set(Param::Arg, param1.as_ref().to_string_lossy()); - } +/// To cancel an import-/export-progress, drop the future returned by this function. +pub async fn imex( + context: &Context, + what: ImexMode, + param1: Option>, +) -> Result<()> { + use futures::future::FutureExt; - job_kill_action(context, Action::ImexImap); - job_add(context, Action::ImexImap, 0, param, 0); + let cancel = context.alloc_ongoing().await?; + let res = imex_inner(context, what, param1) + .race(cancel.recv().map(|_| Err(format_err!("canceled")))) + .await; + + context.free_ongoing().await; + + res } /// Returns the filename of the backup found (otherwise an error) -pub fn has_backup(context: &Context, dir_name: impl AsRef) -> Result { +pub async fn has_backup(context: &Context, dir_name: impl AsRef) -> Result { let dir_name = dir_name.as_ref(); - let dir_iter = std::fs::read_dir(dir_name)?; + let mut dir_iter = async_std::fs::read_dir(dir_name).await?; let mut newest_backup_time = 0; - let mut newest_backup_path: Option = None; - for dirent in dir_iter { + let mut newest_backup_path: Option = None; + while let Some(dirent) = dir_iter.next().await { if let Ok(dirent) = dirent { let path = dirent.path(); let name = dirent.file_name(); let name = name.to_string_lossy(); if name.starts_with("delta-chat") && name.ends_with(".bak") { let sql = Sql::new(); - if sql.open(context, &path, true) { + if sql.open(context, &path, true).await { let curr_backup_time = sql .get_raw_config_int(context, "backup_time") + .await .unwrap_or_default(); if curr_backup_time > newest_backup_time { newest_backup_path = Some(path); newest_backup_time = curr_backup_time; } info!(context, "backup_time of {} is {}", name, curr_backup_time); - sql.close(&context); + sql.close().await; } } } @@ -113,28 +116,32 @@ pub fn has_backup(context: &Context, dir_name: impl AsRef) -> Result Result { - ensure!(context.alloc_ongoing(), "could not allocate ongoing"); - let res = do_initiate_key_transfer(context); - context.free_ongoing(); +pub async fn initiate_key_transfer(context: &Context) -> Result { + use futures::future::FutureExt; + + let cancel = context.alloc_ongoing().await?; + let res = do_initiate_key_transfer(context) + .race(cancel.recv().map(|_| Err(format_err!("canceled")))) + .await; + + context.free_ongoing().await; res } -fn do_initiate_key_transfer(context: &Context) -> Result { +async fn do_initiate_key_transfer(context: &Context) -> Result { let mut msg: Message; let setup_code = create_setup_code(context); /* this may require a keypair to be created. this may take a second ... */ - ensure!(!context.shall_stop_ongoing(), "canceled"); - let setup_file_content = render_setup_file(context, &setup_code)?; + let setup_file_content = render_setup_file(context, &setup_code).await?; /* encrypting may also take a while ... */ - ensure!(!context.shall_stop_ongoing(), "canceled"); let setup_file_blob = BlobObject::create( context, "autocrypt-setup-message.html", setup_file_content.as_bytes(), - )?; + ) + .await?; - let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?; + let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF).await?; msg = Message::default(); msg.viewtype = Viewtype::File; msg.param.set(Param::File, setup_file_blob.as_name()); @@ -147,12 +154,11 @@ fn do_initiate_key_transfer(context: &Context) -> Result { ForcePlaintext::NoAutocryptHeader as i32, ); - ensure!(!context.shall_stop_ongoing(), "canceled"); - let msg_id = chat::send_msg(context, chat_id, &mut msg)?; + let msg_id = chat::send_msg(context, chat_id, &mut msg).await?; info!(context, "Wait for setup message being sent ...",); - while !context.shall_stop_ongoing() { - std::thread::sleep(std::time::Duration::from_secs(1)); - if let Ok(msg) = Message::load_from_db(context, msg_id) { + while !context.shall_stop_ongoing().await { + async_std::task::sleep(std::time::Duration::from_secs(1)).await; + if let Ok(msg) = Message::load_from_db(context, msg_id).await { if msg.is_sent() { info!(context, "... setup message sent.",); break; @@ -170,18 +176,18 @@ fn do_initiate_key_transfer(context: &Context) -> Result { /// Renders HTML body of a setup file message. /// /// The `passphrase` must be at least 2 characters long. -pub fn render_setup_file(context: &Context, passphrase: &str) -> Result { +pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result { ensure!( passphrase.len() >= 2, "Passphrase must be at least 2 chars long." ); - let private_key = Key::from(SignedSecretKey::load_self(context)?); - let ac_headers = match context.get_config_bool(Config::E2eeEnabled) { + let private_key = Key::from(SignedSecretKey::load_self(context).await?); + let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await { false => None, true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), }; let private_key_asc = private_key.to_asc(ac_headers); - let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes())?; + let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes()).await?; let replacement = format!( concat!( @@ -193,8 +199,8 @@ pub fn render_setup_file(context: &Context, passphrase: &str) -> Result ); let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement); - let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject); - let msg_body = context.stock_str(StockMessage::AcSetupMsgBody); + let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await; + let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await; let msg_body_html = msg_body.replace("\r", "").replace("\n", "
"); Ok(format!( concat!( @@ -237,8 +243,8 @@ pub fn create_setup_code(_context: &Context) -> String { ret } -fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { - if !context.sql.get_raw_config_bool(context, "bcc_self") { +async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { + if !context.sql.get_raw_config_bool(context, "bcc_self").await { let mut msg = Message::new(Viewtype::Text); // TODO: define this as a stockstring once the wording is settled. msg.text = Some( @@ -247,26 +253,30 @@ fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { go to the settings and enable \"Send copy to self\"." .to_string(), ); - chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?; + chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?; } Ok(()) } -pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> { +pub async fn continue_key_transfer( + context: &Context, + msg_id: MsgId, + setup_code: &str, +) -> Result<()> { ensure!(!msg_id.is_special(), "wrong id"); - let msg = Message::load_from_db(context, msg_id)?; + let msg = Message::load_from_db(context, msg_id).await?; ensure!( msg.is_setupmessage(), "Message is no Autocrypt Setup Message." ); if let Some(filename) = msg.get_file(context) { - let file = dc_open_file(context, filename)?; + let file = dc_open_file_std(context, filename)?; let sc = normalize_setup_code(setup_code); - let armored_key = decrypt_setup_file(context, &sc, file)?; - set_self_key(context, &armored_key, true, true)?; - maybe_add_bcc_self_device_msg(context)?; + let armored_key = decrypt_setup_file(&sc, file).await?; + set_self_key(context, &armored_key, true, true).await?; + maybe_add_bcc_self_device_msg(context).await?; Ok(()) } else { @@ -274,7 +284,7 @@ pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) } } -fn set_self_key( +async fn set_self_key( context: &Context, armored: &str, set_default: bool, @@ -299,7 +309,8 @@ fn set_self_key( }; context .sql - .set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)?; + .set_raw_config_int(context, "e2ee_enabled", e2ee_enabled) + .await?; } None => { if prefer_encrypt_required { @@ -308,7 +319,7 @@ fn set_self_key( } }; - let self_addr = context.get_config(Config::ConfiguredAddr); + 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())?; @@ -329,16 +340,16 @@ fn set_self_key( } else { key::KeyPairUse::ReadOnly }, - )?; + ) + .await?; Ok(()) } -fn decrypt_setup_file( - _context: &Context, +async fn decrypt_setup_file( passphrase: &str, file: T, ) -> Result { - let plain_bytes = pgp::symm_decrypt(passphrase, file)?; + let plain_bytes = pgp::symm_decrypt(passphrase, file).await?; let plain_text = std::string::String::from_utf8(plain_bytes)?; Ok(plain_text) @@ -357,52 +368,50 @@ pub fn normalize_setup_code(s: &str) -> String { out } -#[allow(non_snake_case)] -pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> { - ensure!(context.alloc_ongoing(), "could not allocate ongoing"); - let what: Option = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32); - let param = job.param.get(Param::Arg).unwrap_or_default(); +async fn imex_inner( + context: &Context, + what: ImexMode, + param: Option>, +) -> Result<()> { + ensure!(param.is_some(), "No Import/export dir/file given."); - ensure!(!param.is_empty(), "No Import/export dir/file given."); info!(context, "Import/export process started."); - context.call_cb(Event::ImexProgress(10)); + context.emit_event(Event::ImexProgress(10)); - ensure!(context.sql.is_open(), "Database not opened."); - if what == Some(ImexMode::ExportBackup) || what == Some(ImexMode::ExportSelfKeys) { + ensure!(context.sql.is_open().await, "Database not opened."); + + let path = param.unwrap(); + if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys { // before we export anything, make sure the private key exists - if e2ee::ensure_secret_key_exists(context).is_err() { - context.free_ongoing(); + if e2ee::ensure_secret_key_exists(context).await.is_err() { bail!("Cannot create private key or private key not available."); } else { - dc_create_folder(context, ¶m)?; + dc_create_folder(context, &path).await?; } } - let path = Path::new(param); + let success = match what { - Some(ImexMode::ExportSelfKeys) => export_self_keys(context, path), - Some(ImexMode::ImportSelfKeys) => import_self_keys(context, path), - Some(ImexMode::ExportBackup) => export_backup(context, path), - Some(ImexMode::ImportBackup) => import_backup(context, path), - None => { - bail!("unknown IMEX type"); - } + ImexMode::ExportSelfKeys => export_self_keys(context, path).await, + ImexMode::ImportSelfKeys => import_self_keys(context, path).await, + ImexMode::ExportBackup => export_backup(context, path).await, + ImexMode::ImportBackup => import_backup(context, path).await, }; - context.free_ongoing(); + match success { Ok(()) => { info!(context, "IMEX successfully completed"); - context.call_cb(Event::ImexProgress(1000)); + context.emit_event(Event::ImexProgress(1000)); Ok(()) } Err(err) => { - context.call_cb(Event::ImexProgress(0)); + context.emit_event(Event::ImexProgress(0)); bail!("IMEX FAILED to complete: {}", err); } } } /// Import Backup -fn import_backup(context: &Context, backup_to_import: impl AsRef) -> Result<()> { +async fn import_backup(context: &Context, backup_to_import: impl AsRef) -> Result<()> { info!( context, "Import \"{}\" to \"{}\".", @@ -411,84 +420,93 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef) -> Resul ); ensure!( - !context.is_configured(), + !context.is_configured().await, "Cannot import backups to accounts in use." ); - context.sql.close(&context); - dc_delete_file(context, context.get_dbfile()); + context.sql.close().await; + dc_delete_file(context, context.get_dbfile()).await; ensure!( - !context.get_dbfile().exists(), + !context.get_dbfile().exists().await, "Cannot delete old database." ); ensure!( - dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()), + dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()).await, "could not copy file" ); /* error already logged */ /* re-open copied database file */ ensure!( - context.sql.open(&context, &context.get_dbfile(), false), + context + .sql + .open(&context, &context.get_dbfile(), false) + .await, "could not re-open db" ); - delete_and_reset_all_device_msgs(&context)?; + delete_and_reset_all_device_msgs(&context).await?; let total_files_cnt = context .sql - .query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![]) + .query_get_value::(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![]) + .await .unwrap_or_default() as usize; info!( context, "***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt, ); - let res = context.sql.query_map( - "SELECT file_name, file_content FROM backup_blobs ORDER BY id;", - params![], - |row| { - let name: String = row.get(0)?; - let blob: Vec = row.get(1)?; + let files = context + .sql + .query_map( + "SELECT file_name, file_content FROM backup_blobs ORDER BY id;", + paramsv![], + |row| { + let name: String = row.get(0)?; + let blob: Vec = row.get(1)?; - Ok((name, blob)) - }, - |files| { - for (processed_files_cnt, file) in files.enumerate() { - let (file_name, file_blob) = file?; - if context.shall_stop_ongoing() { - return Ok(false); - } - let mut permille = processed_files_cnt * 1000 / total_files_cnt; - if permille < 10 { - permille = 10 - } - if permille > 990 { - permille = 990 - } - context.call_cb(Event::ImexProgress(permille)); - if file_blob.is_empty() { - continue; - } + Ok((name, blob)) + }, + |files| { + files + .collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; - let path_filename = context.get_blobdir().join(file_name); - dc_write_file(context, &path_filename, &file_blob)?; - } - Ok(true) - }, - ); - - match res { - Ok(all_files_extracted) => { - if all_files_extracted { - // only delete backup_blobs if all files were successfully extracted - sql::execute(context, &context.sql, "DROP TABLE backup_blobs;", params![])?; - sql::try_execute(context, &context.sql, "VACUUM;").ok(); - Ok(()) - } else { - bail!("received stop signal"); - } + let mut all_files_extracted = true; + for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() { + if context.shall_stop_ongoing().await { + all_files_extracted = false; + break; } - Err(err) => Err(err.into()), + let mut permille = processed_files_cnt * 1000 / total_files_cnt; + if permille < 10 { + permille = 10 + } + if permille > 990 { + permille = 990 + } + context.emit_event(Event::ImexProgress(permille)); + if file_blob.is_empty() { + continue; + } + + let path_filename = context.get_blobdir().join(file_name); + dc_write_file(context, &path_filename, &file_blob).await?; + } + + if all_files_extracted { + // only delete backup_blobs if all files were successfully extracted + context + .sql + .execute("DROP TABLE backup_blobs;", paramsv![]) + .await?; + context.sql.execute("VACUUM;", paramsv![]).await.ok(); + Ok(()) + } else { + bail!("received stop signal"); } } @@ -497,28 +515,31 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef) -> Resul ******************************************************************************/ /* the FILE_PROGRESS macro calls the callback with the permille of files processed. The macro avoids weird values of 0% or 100% while still working. */ -fn export_backup(context: &Context, dir: impl AsRef) -> Result<()> { +async fn export_backup(context: &Context, dir: impl AsRef) -> Result<()> { // get a fine backup file name (the name includes the date so that multiple backup instances are possible) // FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete. // let dest_path_filename = dc_get_next_backup_file(context, dir, res); let now = time(); - let dest_path_filename = dc_get_next_backup_path(dir, now)?; + let dest_path_filename = dc_get_next_backup_path(dir, now).await?; let dest_path_string = dest_path_filename.to_string_lossy().to_string(); - sql::housekeeping(context); + sql::housekeeping(context).await; - sql::try_execute(context, &context.sql, "VACUUM;").ok(); + context.sql.execute("VACUUM;", paramsv![]).await.ok(); // we close the database during the copy of the dbfile - context.sql.close(context); + context.sql.close().await; info!( context, "Backup '{}' to '{}'.", context.get_dbfile().display(), dest_path_filename.display(), ); - let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename); - context.sql.open(&context, &context.get_dbfile(), false); + let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename).await; + context + .sql + .open(&context, &context.get_dbfile(), false) + .await; if !copied { bail!( @@ -529,86 +550,90 @@ fn export_backup(context: &Context, dir: impl AsRef) -> Result<()> { } let dest_sql = Sql::new(); ensure!( - dest_sql.open(context, &dest_path_filename, false), + dest_sql.open(context, &dest_path_filename, false).await, "could not open exported database {}", dest_path_string ); - let res = match add_files_to_export(context, &dest_sql) { + let res = match add_files_to_export(context, &dest_sql).await { Err(err) => { - dc_delete_file(context, &dest_path_filename); + dc_delete_file(context, &dest_path_filename).await; error!(context, "backup failed: {}", err); Err(err) } Ok(()) => { - dest_sql.set_raw_config_int(context, "backup_time", now as i32)?; - context.call_cb(Event::ImexFileWritten(dest_path_filename)); + dest_sql + .set_raw_config_int(context, "backup_time", now as i32) + .await?; + context.emit_event(Event::ImexFileWritten(dest_path_filename)); Ok(()) } }; - dest_sql.close(context); + dest_sql.close().await; Ok(res?) } -fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> { +async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> { // add all files as blobs to the database copy (this does not require // the source to be locked, neigher the destination as it is used only here) - if !sql.table_exists("backup_blobs") { - sql::execute( - context, - &sql, + if !sql.table_exists("backup_blobs").await? { + sql.execute( "CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);", - params![], - )? + paramsv![], + ) + .await?; } // copy all files from BLOBDIR into backup-db let mut total_files_cnt = 0; let dir = context.get_blobdir(); - let dir_handle = std::fs::read_dir(&dir)?; - total_files_cnt += dir_handle.filter(|r| r.is_ok()).count(); + let dir_handle = async_std::fs::read_dir(&dir).await?; + total_files_cnt += dir_handle.filter(|r| r.is_ok()).count().await; info!(context, "EXPORT: total_files_cnt={}", total_files_cnt); - // scan directory, pass 2: copy files - let dir_handle = std::fs::read_dir(&dir)?; - let exported_all_files = sql.prepare( - "INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);", - |mut stmt, _| { - let mut processed_files_cnt = 0; - for entry in dir_handle { - let entry = entry?; - if context.shall_stop_ongoing() { - return Ok(false); - } - processed_files_cnt += 1; - let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10); - context.call_cb(Event::ImexProgress(permille)); - let name_f = entry.file_name(); - let name = name_f.to_string_lossy(); - if name.starts_with("delta-chat") && name.ends_with(".bak") { + sql.with_conn_async(|conn| async move { + // scan directory, pass 2: copy files + let mut dir_handle = async_std::fs::read_dir(&dir).await?; + + let mut processed_files_cnt = 0; + while let Some(entry) = dir_handle.next().await { + let entry = entry?; + if context.shall_stop_ongoing().await { + return Ok(()); + } + processed_files_cnt += 1; + let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10); + context.emit_event(Event::ImexProgress(permille)); + + let name_f = entry.file_name(); + let name = name_f.to_string_lossy(); + if name.starts_with("delta-chat") && name.ends_with(".bak") { + continue; + } + info!(context, "EXPORT: copying filename={}", name); + let curr_path_filename = context.get_blobdir().join(entry.file_name()); + if let Ok(buf) = dc_read_file(context, &curr_path_filename).await { + if buf.is_empty() { continue; } - info!(context, "EXPORT: copying filename={}", name); - let curr_path_filename = context.get_blobdir().join(entry.file_name()); - if let Ok(buf) = dc_read_file(context, &curr_path_filename) { - if buf.is_empty() { - continue; - } - // bail out if we can't insert - stmt.execute(params![name, buf])?; - } + // bail out if we can't insert + let mut stmt = conn.prepare_cached( + "INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);", + )?; + stmt.execute(paramsv![name, buf])?; } - Ok(true) - }, - )?; - ensure!(exported_all_files, "canceled during export-files"); + } + Ok(()) + }) + .await?; + Ok(()) } /******************************************************************************* * Classic key import ******************************************************************************/ -fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { +async fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { /* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import plain ASC keys, at least keys without a password, if we do not want to implement a password entry function. Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation. @@ -619,8 +644,8 @@ fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { let mut imported_cnt = 0; let dir_name = dir.as_ref().to_string_lossy(); - let dir_handle = std::fs::read_dir(&dir)?; - for entry in dir_handle { + let mut dir_handle = async_std::fs::read_dir(&dir).await?; + while let Some(entry) = dir_handle.next().await { let entry_fn = entry?.file_name(); let name_f = entry_fn.to_string_lossy(); let path_plus_name = dir.as_ref().join(&entry_fn); @@ -640,10 +665,10 @@ fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { continue; } } - match dc_read_file(context, &path_plus_name) { + match dc_read_file(context, &path_plus_name).await { Ok(buf) => { let armored = std::string::String::from_utf8_lossy(&buf); - if let Err(err) = set_self_key(context, &armored, set_default, false) { + if let Err(err) = set_self_key(context, &armored, set_default, false).await { error!(context, "set_self_key: {}", err); continue; } @@ -660,45 +685,54 @@ fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { Ok(()) } -fn export_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { +async fn export_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { let mut export_errors = 0; - context.sql.query_map( - "SELECT id, public_key, private_key, is_default FROM keypairs;", - params![], - |row| { - let id = row.get(0)?; - let public_key_blob: Vec = row.get(1)?; - let public_key = Key::from_slice(&public_key_blob, KeyType::Public); - let private_key_blob: Vec = row.get(2)?; - let private_key = Key::from_slice(&private_key_blob, KeyType::Private); - let is_default: i32 = row.get(3)?; + let keys = context + .sql + .query_map( + "SELECT id, public_key, private_key, is_default FROM keypairs;", + paramsv![], + |row| { + let id = row.get(0)?; + let public_key_blob: Vec = row.get(1)?; + let public_key = Key::from_slice(&public_key_blob, KeyType::Public); + let private_key_blob: Vec = row.get(2)?; + let private_key = Key::from_slice(&private_key_blob, KeyType::Private); + let is_default: i32 = row.get(3)?; - Ok((id, public_key, private_key, is_default)) - }, - |keys| { - for key_pair in keys { - let (id, public_key, private_key, is_default) = key_pair?; - let id = Some(id).filter(|_| is_default != 0); - if let Some(key) = public_key { - if export_key_to_asc_file(context, &dir, id, &key).is_err() { - export_errors += 1; - } - } else { - export_errors += 1; - } - if let Some(key) = private_key { - if export_key_to_asc_file(context, &dir, id, &key).is_err() { - export_errors += 1; - } - } else { - export_errors += 1; - } + Ok((id, public_key, private_key, is_default)) + }, + |keys| { + keys.collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; + + for (id, public_key, private_key, is_default) in keys { + let id = Some(id).filter(|_| is_default != 0); + if let Ok(key) = public_key { + if export_key_to_asc_file(context, &dir, id, &key) + .await + .is_err() + { + export_errors += 1; } - - Ok(()) - }, - )?; + } else { + export_errors += 1; + } + if let Ok(key) = private_key { + if export_key_to_asc_file(context, &dir, id, &key) + .await + .is_err() + { + export_errors += 1; + } + } else { + export_errors += 1; + } + } ensure!(export_errors == 0, "errors while exporting keys"); Ok(()) @@ -707,7 +741,7 @@ fn export_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { /******************************************************************************* * Classic key export ******************************************************************************/ -fn export_key_to_asc_file( +async fn export_key_to_asc_file( context: &Context, dir: impl AsRef, id: Option, @@ -720,13 +754,13 @@ fn export_key_to_asc_file( dir.as_ref().join(format!("{}-key-{}.asc", kind, &id)) }; info!(context, "Exporting key {}", file_name.display()); - dc_delete_file(context, &file_name); + dc_delete_file(context, &file_name).await; - let res = key.write_asc_to_file(&file_name, context); + let res = key.write_asc_to_file(&file_name, context).await; if res.is_err() { error!(context, "Cannot write key to {}", file_name.display()); } else { - context.call_cb(Event::ImexFileWritten(file_name)); + context.emit_event(Event::ImexFileWritten(file_name)); } res } @@ -738,12 +772,12 @@ mod tests { use crate::test_utils::*; use ::pgp::armor::BlockType; - #[test] - fn test_render_setup_file() { - let t = test_context(Some(Box::new(logging_cb))); + #[async_std::test] + async fn test_render_setup_file() { + let t = test_context().await; - configure_alice_keypair(&t.ctx); - let msg = render_setup_file(&t.ctx, "hello").unwrap(); + configure_alice_keypair(&t.ctx).await; + let msg = render_setup_file(&t.ctx, "hello").await.unwrap(); println!("{}", &msg); // Check some substrings, indicating things got substituted. // In particular note the mixing of `\r\n` and `\n` depending @@ -757,21 +791,22 @@ mod tests { assert!(msg.contains("-----END PGP MESSAGE-----\n")); } - #[test] - fn test_render_setup_file_newline_replace() { - let t = dummy_context(); + #[async_std::test] + async fn test_render_setup_file_newline_replace() { + let t = dummy_context().await; t.ctx .set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string()) + .await .unwrap(); - configure_alice_keypair(&t.ctx); - let msg = render_setup_file(&t.ctx, "pw").unwrap(); + configure_alice_keypair(&t.ctx).await; + let msg = render_setup_file(&t.ctx, "pw").await.unwrap(); println!("{}", &msg); assert!(msg.contains("

hello
there

")); } - #[test] - fn test_create_setup_code() { - let t = dummy_context(); + #[async_std::test] + async fn test_create_setup_code() { + let t = dummy_context().await; let setupcode = create_setup_code(&t.ctx); assert_eq!(setupcode.len(), 44); assert_eq!(setupcode.chars().nth(4).unwrap(), '-'); @@ -784,15 +819,17 @@ mod tests { assert_eq!(setupcode.chars().nth(39).unwrap(), '-'); } - #[test] - fn test_export_key_to_asc_file() { - let context = dummy_context(); + #[async_std::test] + async fn test_export_key_to_asc_file() { + let context = dummy_context().await; let key = Key::from(alice_keypair().public); let blobdir = "$BLOBDIR"; - assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok()); + assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key) + .await + .is_ok()); let blobdir = context.ctx.get_blobdir().to_str().unwrap(); let filename = format!("{}/public-key-default.asc", blobdir); - let bytes = std::fs::read(&filename).unwrap(); + let bytes = async_std::fs::read(&filename).await.unwrap(); assert_eq!(bytes, key.to_asc(None).into_bytes()); } @@ -812,11 +849,8 @@ mod tests { const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597"; const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt"); - #[test] - fn test_split_and_decrypt() { - let ctx = dummy_context(); - let context = &ctx.ctx; - + #[async_std::test] + async fn test_split_and_decrypt() { let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec(); let (typ, headers, base64) = split_armored_data(&buf_1).unwrap(); assert_eq!(typ, BlockType::Message); @@ -826,12 +860,10 @@ mod tests { assert!(!base64.is_empty()); let setup_file = S_EM_SETUPFILE.to_string(); - let decrypted = decrypt_setup_file( - context, - S_EM_SETUPCODE, - std::io::Cursor::new(setup_file.as_bytes()), - ) - .unwrap(); + let decrypted = + decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes())) + .await + .unwrap(); let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap(); diff --git a/src/job.rs b/src/job.rs index 32eea25fd..7249cb0e5 100644 --- a/src/job.rs +++ b/src/job.rs @@ -3,7 +3,8 @@ //! This module implements a job queue maintained in the SQLite database //! and job types. -use std::{fmt, time}; +use std::fmt; +use std::future::Future; use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; @@ -12,26 +13,24 @@ use rand::{thread_rng, Rng}; use async_smtp::smtp::response::Category; use async_smtp::smtp::response::Code; use async_smtp::smtp::response::Detail; -use async_std::task; use crate::blob::BlobObject; use crate::chat::{self, ChatId}; use crate::config::Config; -use crate::configure::*; use crate::constants::*; use crate::contact::Contact; -use crate::context::{Context, PerformJobsNeeded}; +use crate::context::Context; use crate::dc_tools::*; use crate::error::{bail, ensure, format_err, Error, Result}; use crate::events::Event; use crate::imap::*; -use crate::imex::*; use crate::location; use crate::login_param::LoginParam; use crate::message::MsgId; use crate::message::{self, Message, MessageState}; -use crate::mimefactory::{MimeFactory, RenderedEmail}; +use crate::mimefactory::MimeFactory; use crate::param::*; +use crate::smtp::Smtp; use crate::sql; // results in ~3 weeks for the last backoff timespan @@ -40,7 +39,7 @@ const JOB_RETRIES: u32 = 17; /// Thread IDs #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)] #[repr(i32)] -enum Thread { +pub(crate) enum Thread { Unknown = 0, Imap = 100, Smtp = 5000, @@ -58,8 +57,8 @@ pub enum Status { macro_rules! job_try { ($expr:expr) => { match $expr { - ::std::result::Result::Ok(val) => val, - ::std::result::Result::Err(err) => { + std::result::Result::Ok(val) => val, + std::result::Result::Err(err) => { return $crate::job::Status::Finished(Err(err.into())); } } @@ -101,10 +100,7 @@ pub enum Action { // Moving message is prioritized lower than deletion so we don't // bother moving message if it is already scheduled for deletion. MoveMsg = 200, - DeleteMsgOnImap = 210, - ConfigureImap = 900, - ImexImap = 910, // ... high priority // Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999 MaybeSendLocations = 5005, // low priority ... @@ -132,8 +128,6 @@ impl From for Thread { EmptyServer => Thread::Imap, MarkseenMsgOnImap => Thread::Imap, MoveMsg => Thread::Imap, - ConfigureImap => Thread::Imap, - ImexImap => Thread::Imap, MaybeSendLocations => Thread::Smtp, MaybeSendLocationsEnded => Thread::Smtp, @@ -162,7 +156,7 @@ impl fmt::Display for Job { } impl Job { - fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self { + pub fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self { let timestamp = time(); Self { @@ -177,44 +171,47 @@ impl Job { } } + pub fn delay_seconds(&self) -> i64 { + self.desired_timestamp - self.added_timestamp + } + /// Deletes the job from the database. - fn delete(&self, context: &Context) -> bool { + async fn delete(self, context: &Context) -> Result<()> { if self.job_id != 0 { context .sql - .execute("DELETE FROM jobs WHERE id=?;", params![self.job_id as i32]) - .is_ok() - } else { - // Already deleted. - true + .execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32]) + .await?; } + + Ok(()) } /// Saves the job to the database, creating a new entry if necessary. /// /// The Job is consumed by this method. - fn save(self, context: &Context) -> bool { + pub async fn save(self, context: &Context) -> Result<()> { let thread: Thread = self.action.into(); + info!(context, "saving job for {}-thread: {:?}", thread, self); + if self.job_id != 0 { - sql::execute( - context, - &context.sql, - "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", - params![ - self.desired_timestamp, - self.tries as i64, - self.param.to_string(), - self.job_id as i32, - ], - ) - .is_ok() + 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, + ], + ) + .await?; } else { - sql::execute( - context, - &context.sql, + context.sql.execute( "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);", - params![ + paramsv![ self.added_timestamp, thread, self.action, @@ -222,31 +219,34 @@ impl Job { self.param.to_string(), self.desired_timestamp ] - ).is_ok() + ).await?; } + + Ok(()) } - fn smtp_send( + async fn smtp_send( &mut self, context: &Context, recipients: Vec, message: Vec, job_id: u32, + smtp: &mut Smtp, success_cb: F, ) -> Status where - F: FnOnce() -> Result<()>, + F: FnOnce() -> Fut, + Fut: Future>, { // hold the smtp lock during sending of a job and // its ok/error response processing. Note that if a message // was sent we need to mark it in the database ASAP as we // otherwise might send it twice. - let mut smtp = context.smtp.lock().unwrap(); if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { info!(context, "smtp-sending out mime message:"); println!("{}", String::from_utf8_lossy(&message)); } - match task::block_on(smtp.send(context, recipients, message, job_id)) { + 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); @@ -268,12 +268,17 @@ impl Job { _ => { // If we do not retry, add an info message to the chat // Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM. - match Message::load_from_db(context, MsgId::new(self.foreign_id)) { - Ok(message) => chat::add_info_msg( - context, - message.chat_id, - err.to_string(), - ), + match Message::load_from_db(context, MsgId::new(self.foreign_id)) + .await + { + Ok(message) => { + chat::add_info_msg( + context, + message.chat_id, + err.to_string(), + ) + .await + } Err(e) => warn!( context, "couldn't load chat_id to inform user about SMTP error: {}", @@ -291,7 +296,7 @@ impl Job { Status::RetryLater } _ => { - if smtp.has_maybe_stale_connection() { + if smtp.has_maybe_stale_connection().await { info!(context, "stale connection? immediately reconnecting"); Status::RetryNow } else { @@ -301,13 +306,13 @@ impl Job { }; // this clears last_success info - smtp.disconnect(); + smtp.disconnect().await; res } Err(crate::smtp::send::Error::EnvelopeError(err)) => { // Local error, job is invalid, do not retry. - smtp.disconnect(); + smtp.disconnect().await; warn!(context, "SMTP job is invalid: {}", err); Status::Finished(Err(err.into())) } @@ -318,18 +323,17 @@ impl Job { Status::Finished(Err(format_err!("SMTP has not transport"))) } Ok(()) => { - job_try!(success_cb()); + job_try!(success_cb().await); Status::Finished(Ok(())) } } } - #[allow(non_snake_case)] - fn SendMsgToSmtp(&mut self, context: &Context) -> Status { - /* connect to SMTP server, if not yet done */ - if !context.smtp.lock().unwrap().is_connected() { - let loginparam = LoginParam::from_database(context, "configured_"); - if let Err(err) = context.smtp.lock().unwrap().connect(context, &loginparam) { + pub async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status { + // SMTP server, if not yet done + if !smtp.is_connected().await { + let loginparam = LoginParam::from_database(context, "configured_").await; + if let Err(err) = smtp.connect(context, &loginparam).await { warn!(context, "SMTP connection failure: {:?}", err); return Status::RetryLater; } @@ -340,7 +344,7 @@ impl Job { .get_path(Param::File, context) .map_err(|_| format_err!("Can't get filename"))) .ok_or_else(|| format_err!("Can't get filename"))); - let body = job_try!(dc_read_file(context, &filename)); + let body = job_try!(dc_read_file(context, &filename).await); let recipients = job_try!(self.param.get(Param::Recipients).ok_or_else(|| { warn!(context, "Missing recipients for job {}", self.job_id); format_err!("Missing recipients") @@ -362,7 +366,7 @@ 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)) { + 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 @@ -370,50 +374,56 @@ impl Job { }; let foreign_id = self.foreign_id; - self.smtp_send(context, recipients_list, body, self.job_id, || { - // smtp success, update db ASAP, then delete smtp file - if 0 != foreign_id { - set_delivered(context, MsgId::new(foreign_id)); + self.smtp_send(context, recipients_list, body, self.job_id, smtp, || { + async move { + // smtp success, update db ASAP, then delete smtp file + if 0 != foreign_id { + set_delivered(context, MsgId::new(foreign_id)).await; + } + // now also delete the generated file + dc_delete_file(context, filename).await; + Ok(()) } - // now also delete the generated file - dc_delete_file(context, filename); - Ok(()) }) + .await } /// Get `SendMdn` jobs with foreign_id equal to `contact_id` excluding the `job_id` job. - fn get_additional_mdn_jobs( + async fn get_additional_mdn_jobs( &self, context: &Context, contact_id: u32, ) -> sql::Result<(Vec, Vec)> { // Extract message IDs from job parameters - let res: Vec<(u32, MsgId)> = context.sql.query_map( - "SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?", - params![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) - }, - )?; + let res: Vec<(u32, MsgId)> = 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) + }, + ) + .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) { + if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await { job_ids.push(job_id); rfc724_mids.push(rfc724_mid); } @@ -421,16 +431,15 @@ impl Job { Ok((job_ids, rfc724_mids)) } - #[allow(non_snake_case)] - fn SendMdn(&mut self, context: &Context) -> Status { - if !context.get_config_bool(Config::MdnsEnabled) { + async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status { + if !context.get_config_bool(Config::MdnsEnabled).await { // User has disabled MDNs after job scheduling but before // execution. return Status::Finished(Err(format_err!("MDNs are disabled"))); } let contact_id = self.foreign_id; - let contact = job_try!(Contact::load_from_db(context, contact_id)); + let contact = job_try!(Contact::load_from_db(context, contact_id).await); if contact.is_blocked() { return Status::Finished(Err(format_err!("Contact is blocked"))); } @@ -447,6 +456,7 @@ impl Job { // Try to aggregate other SendMdn jobs and send a combined MDN. let (additional_job_ids, additional_rfc724_mids) = self .get_additional_mdn_jobs(context, contact_id) + .await .unwrap_or_default(); if !additional_rfc724_mids.is_empty() { @@ -457,9 +467,10 @@ impl Job { ) } - let msg = job_try!(Message::load_from_db(context, msg_id)); - let mimefactory = job_try!(MimeFactory::from_mdn(context, &msg, additional_rfc724_mids)); - let rendered_msg = job_try!(mimefactory.render()); + let msg = job_try!(Message::load_from_db(context, msg_id).await); + let mimefactory = + job_try!(MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await); + let rendered_msg = job_try!(mimefactory.render().await); let body = rendered_msg.message; let addr = contact.get_addr(); @@ -467,44 +478,48 @@ impl Job { .map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))); let recipients = vec![recipient]; - /* connect to SMTP server, if not yet done */ - if !context.smtp.lock().unwrap().is_connected() { - let loginparam = LoginParam::from_database(context, "configured_"); - if let Err(err) = context.smtp.lock().unwrap().connect(context, &loginparam) { + // connect to SMTP server, if not yet done + if !smtp.is_connected().await { + let loginparam = LoginParam::from_database(context, "configured_").await; + if let Err(err) = smtp.connect(context, &loginparam).await { warn!(context, "SMTP connection failure: {:?}", err); return Status::RetryLater; } } - self.smtp_send(context, recipients, body, self.job_id, || { - // Remove additional SendMdn jobs we have aggregated into this one. - job_kill_ids(context, &additional_job_ids)?; - Ok(()) + self.smtp_send(context, recipients, body, self.job_id, smtp, || { + async move { + // Remove additional SendMdn jobs we have aggregated into this one. + kill_ids(context, &additional_job_ids).await?; + Ok(()) + } }) + .await } - #[allow(non_snake_case)] - fn MoveMsg(&mut self, context: &Context) -> Status { - let imap_inbox = &context.inbox_thread.read().unwrap().imap; + async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status { + let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await); - let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id))); - - if let Err(err) = imap_inbox.ensure_configured_folders(context, true) { + if let Err(err) = imap.ensure_configured_folders(context, true).await { warn!(context, "could not configure folders: {:?}", err); return Status::RetryLater; } let dest_folder = context .sql - .get_raw_config(context, "configured_mvbox_folder"); + .get_raw_config(context, "configured_mvbox_folder") + .await; if let Some(dest_folder) = dest_folder { let server_folder = msg.server_folder.as_ref().unwrap(); - match imap_inbox.mv(context, server_folder, msg.server_uid, &dest_folder) { + match imap + .mv(context, server_folder, msg.server_uid, &dest_folder) + .await + { ImapActionResult::RetryLater => Status::RetryLater, ImapActionResult::Success => { // XXX Rust-Imap provides no target uid on mv, so just set it to 0 - message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0); + message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await; Status::Finished(Ok(())) } ImapActionResult::Failed => { @@ -525,14 +540,11 @@ impl Job { /// This job removes the database record. If there are no more /// records pointing to the same message on the server, the job /// also removes the message on the server. - #[allow(non_snake_case)] - fn DeleteMsgOnImap(&mut self, context: &Context) -> Status { - let imap_inbox = &context.inbox_thread.read().unwrap().imap; - - let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id))); + async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status { + let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await); if !msg.rfc724_mid.is_empty() { - let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid); + let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid).await; info!( context, "Running delete job for message {} which has {} entries in the database", @@ -553,7 +565,8 @@ impl Job { // Message is already deleted on IMAP server. ImapActionResult::AlreadyDone } else { - imap_inbox.delete_msg(context, &mid, server_folder, msg.server_uid) + imap.delete_msg(context, &mid, server_folder, msg.server_uid) + .await }; match res { ImapActionResult::AlreadyDone | ImapActionResult::Success => {} @@ -578,7 +591,7 @@ impl Job { // Hidden messages are similar to trashed, but are // related to some chat. We also delete their // database records. - job_try!(msg.id.delete_from_db(context)) + job_try!(msg.id.delete_from_db(context).await) } else { // Remove server UID from the database record. // @@ -588,7 +601,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. - job_try!(msg.id.unlink(context)); + job_try!(msg.id.unlink(context).await); } Status::Finished(Ok(())) } else { @@ -597,31 +610,27 @@ impl Job { } } - #[allow(non_snake_case)] - fn EmptyServer(&mut self, context: &Context) -> Status { - let imap_inbox = &context.inbox_thread.read().unwrap().imap; + async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status { if self.foreign_id & DC_EMPTY_MVBOX > 0 { if let Some(mvbox_folder) = context .sql .get_raw_config(context, "configured_mvbox_folder") + .await { - imap_inbox.empty_folder(context, &mvbox_folder); + imap.empty_folder(context, &mvbox_folder).await; } } if self.foreign_id & DC_EMPTY_INBOX > 0 { - imap_inbox.empty_folder(context, "INBOX"); + imap.empty_folder(context, "INBOX").await; } Status::Finished(Ok(())) } - #[allow(non_snake_case)] - fn MarkseenMsgOnImap(&mut self, context: &Context) -> Status { - let imap_inbox = &context.inbox_thread.read().unwrap().imap; - - let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id))); + async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status { + let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await); let folder = msg.server_folder.as_ref().unwrap(); - match imap_inbox.set_seen(context, folder, msg.server_uid) { + match imap.set_seen(context, folder, msg.server_uid).await { ImapActionResult::RetryLater => Status::RetryLater, ImapActionResult::AlreadyDone => Status::Finished(Ok(())), ImapActionResult::Success | ImapActionResult::Failed => { @@ -630,9 +639,9 @@ impl Job { // The job will not be retried so locally // there is no risk of double-sending MDNs. if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() - && context.get_config_bool(Config::MdnsEnabled) + && context.get_config_bool(Config::MdnsEnabled).await { - if let Err(err) = send_mdn(context, &msg) { + 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)); } @@ -643,261 +652,65 @@ impl Job { } } -/* delete all pending jobs with the given action */ -pub fn job_kill_action(context: &Context, action: Action) -> bool { - sql::execute( - context, - &context.sql, - "DELETE FROM jobs WHERE action=?;", - params![action], - ) - .is_ok() +/// Delete all pending jobs with the given action. +pub async fn kill_action(context: &Context, action: Action) -> bool { + context + .sql + .execute("DELETE FROM jobs WHERE action=?;", paramsv![action]) + .await + .is_ok() } /// Remove jobs with specified IDs. -pub fn job_kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> { - sql::execute( - context, - &context.sql, - format!( - "DELETE FROM jobs WHERE id IN({})", - job_ids.iter().map(|_| "?").join(",") - ), - job_ids, - ) -} - -pub fn perform_inbox_fetch(context: &Context) { - let use_network = context.get_config_bool(Config::InboxWatch); - - task::block_on( - context - .inbox_thread - .write() - .unwrap() - .fetch(context, use_network), - ); -} - -pub fn perform_mvbox_fetch(context: &Context) { - let use_network = context.get_config_bool(Config::MvboxWatch); - - task::block_on( - context - .mvbox_thread - .write() - .unwrap() - .fetch(context, use_network), - ); -} - -pub fn perform_sentbox_fetch(context: &Context) { - let use_network = context.get_config_bool(Config::SentboxWatch); - - task::block_on( - context - .sentbox_thread - .write() - .unwrap() - .fetch(context, use_network), - ); -} - -pub fn perform_inbox_idle(context: &Context) { - if *context.perform_inbox_jobs_needed.clone().read().unwrap() { - info!( - context, - "INBOX-IDLE will not be started because of waiting jobs." - ); - return; - } - let use_network = context.get_config_bool(Config::InboxWatch); - +async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> { context - .inbox_thread - .read() - .unwrap() - .idle(context, use_network); -} - -pub fn perform_mvbox_idle(context: &Context) { - let use_network = context.get_config_bool(Config::MvboxWatch); - - context - .mvbox_thread - .read() - .unwrap() - .idle(context, use_network); -} - -pub fn perform_sentbox_idle(context: &Context) { - let use_network = context.get_config_bool(Config::SentboxWatch); - - context - .sentbox_thread - .read() - .unwrap() - .idle(context, use_network); -} - -pub fn interrupt_inbox_idle(context: &Context) { - info!(context, "interrupt_inbox_idle called"); - // we do not block on trying to obtain the thread lock - // because we don't know in which state the thread is. - // If it's currently fetching then we can not get the lock - // but we flag it for checking jobs so that idle will be skipped. - match context.inbox_thread.try_read() { - Ok(inbox_thread) => { - inbox_thread.interrupt_idle(context); - } - Err(err) => { - *context.perform_inbox_jobs_needed.write().unwrap() = true; - warn!(context, "could not interrupt idle: {}", err); - } - } -} - -pub fn interrupt_mvbox_idle(context: &Context) { - context.mvbox_thread.read().unwrap().interrupt_idle(context); -} - -pub fn interrupt_sentbox_idle(context: &Context) { - context - .sentbox_thread - .read() - .unwrap() - .interrupt_idle(context); -} - -pub fn perform_smtp_jobs(context: &Context) { - let probe_smtp_network = { - let &(ref lock, _) = &*context.smtp_state.clone(); - let mut state = lock.lock().unwrap(); - - let probe_smtp_network = state.probe_network; - state.probe_network = false; - state.perform_jobs_needed = PerformJobsNeeded::Not; - - if state.suspended { - info!(context, "SMTP-jobs suspended.",); - return; - } - state.doing_jobs = true; - probe_smtp_network - }; - - info!(context, "SMTP-jobs started...",); - job_perform(context, Thread::Smtp, probe_smtp_network); - info!(context, "SMTP-jobs ended."); - - { - let &(ref lock, _) = &*context.smtp_state.clone(); - let mut state = lock.lock().unwrap(); - - state.doing_jobs = false; - } -} - -pub fn perform_smtp_idle(context: &Context) { - info!(context, "SMTP-idle started...",); - { - let &(ref lock, ref cvar) = &*context.smtp_state.clone(); - let mut state = lock.lock().unwrap(); - - match state.perform_jobs_needed { - PerformJobsNeeded::AtOnce => { - info!( - context, - "SMTP-idle will not be started because of waiting jobs.", - ); - } - PerformJobsNeeded::Not | PerformJobsNeeded::AvoidDos => { - let dur = get_next_wakeup_time(context, Thread::Smtp); - - loop { - let res = cvar.wait_timeout(state, dur).unwrap(); - state = res.0; - - if state.idle || res.1.timed_out() { - // We received the notification and the value has been updated, we can leave. - break; - } - } - state.idle = false; - } - } - } - - info!(context, "SMTP-idle ended.",); -} - -fn get_next_wakeup_time(context: &Context, thread: Thread) -> time::Duration { - let t: i64 = context .sql - .query_get_value( - context, - "SELECT MIN(desired_timestamp) FROM jobs WHERE thread=?;", - params![thread], + .execute( + format!( + "DELETE FROM jobs WHERE id IN({})", + job_ids.iter().map(|_| "?").join(",") + ), + job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(), ) - .unwrap_or_default(); - - let mut wakeup_time = time::Duration::new(10 * 60, 0); - let now = time(); - if t > 0 { - if t > now { - wakeup_time = time::Duration::new((t - now) as u64, 0); - } else { - wakeup_time = time::Duration::new(0, 0); - } - } - - wakeup_time + .await?; + Ok(()) } -pub fn maybe_network(context: &Context) { - { - let &(ref lock, _) = &*context.smtp_state.clone(); - let mut state = lock.lock().unwrap(); - state.probe_network = true; - - *context.probe_imap_network.write().unwrap() = true; - } - - interrupt_smtp_idle(context); - interrupt_inbox_idle(context); - interrupt_mvbox_idle(context); - interrupt_sentbox_idle(context); -} - -pub fn job_action_exists(context: &Context, action: Action) -> bool { +pub async fn action_exists(context: &Context, action: Action) -> bool { context .sql - .exists("SELECT id FROM jobs WHERE action=?;", params![action]) + .exists("SELECT id FROM jobs WHERE action=?;", paramsv![action]) + .await .unwrap_or_default() } -fn set_delivered(context: &Context, msg_id: MsgId) { - message::update_msg_state(context, msg_id, MessageState::OutDelivered); +async fn set_delivered(context: &Context, msg_id: MsgId) { + 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=?", - params![msg_id], + paramsv![msg_id], ) + .await .unwrap_or_default(); - context.call_cb(Event::MsgDelivered { chat_id, msg_id }); + context.emit_event(Event::MsgDelivered { chat_id, msg_id }); } -/* special case for DC_JOB_SEND_MSG_TO_SMTP */ -pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { - let mut msg = Message::load_from_db(context, msg_id)?; - msg.try_calc_and_set_dimensions(context).ok(); +/// Constructs a job for sending a message. +/// +/// Returns `None` if no messages need to be sent out. +/// +/// In order to be processed, must be `add`ded. +pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result> { + let mut msg = Message::load_from_db(context, msg_id).await?; + msg.try_calc_and_set_dimensions(context).await.ok(); /* create message */ let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default(); - let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id) { + let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id).await { Ok(attach_selfavatar) => attach_selfavatar, Err(err) => { warn!(context, "job: cannot get selfavatar-state: {}", err); @@ -905,19 +718,20 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { } }; - let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?; + let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?; let mut recipients = mimefactory.recipients(); let from = context .get_config(Config::ConfiguredAddr) + .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) - && context.get_config_delete_server_after() != 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) @@ -931,14 +745,17 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { context, "message {} has no recipient, skipping smtp-send", msg_id ); - set_delivered(context, msg_id); - return Ok(()); + set_delivered(context, msg_id).await; + return Ok(None); } - let rendered_msg = mimefactory.render().map_err(|err| { - message::set_msg_failed(context, msg_id, Some(err.to_string())); - err - })?; + let rendered_msg = match mimefactory.render().await { + Ok(res) => Ok(res), + Err(err) => { + message::set_msg_failed(context, msg_id, Some(err.to_string())).await; + Err(err) + } + }?; if needs_encryption && !rendered_msg.is_encrypted { /* unrecoverable */ @@ -946,7 +763,8 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { context, msg_id, Some("End-to-end-encryption unavailable unexpectedly."), - ); + ) + .await; bail!( "e2e encryption unavailable {} - {:?}", msg_id, @@ -955,16 +773,17 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { } if rendered_msg.is_gossiped { - chat::set_gossiped_timestamp(context, msg.chat_id, time())?; + chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?; } if 0 != rendered_msg.last_added_location_id { - if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()) { + if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await { error!(context, "Failed to set kml sent_timestamp: {:?}", err); } if !msg.hidden { if let Err(err) = location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id) + .await { error!(context, "Failed to set msg_location_id: {:?}", err); } @@ -972,45 +791,57 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> { } if attach_selfavatar { - if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()) { + if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await { error!(context, "Failed to set selfavatar timestamp: {:?}", err); } } if rendered_msg.is_encrypted && !needs_encryption { msg.param.set_int(Param::GuaranteeE2ee, 1); - msg.save_param_to_disk(context); + msg.save_param_to_disk(context).await; } - add_smtp_job( - context, - Action::SendMsgToSmtp, - msg.id, - recipients, - &rendered_msg, - )?; + ensure!(!recipients.is_empty(), "no recipients for smtp job set"); + let mut param = Params::new(); + let bytes = &rendered_msg.message; + let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes).await?; - Ok(()) + let recipients = recipients.join("\x1e"); + param.set(Param::File, blob.as_name()); + param.set(Param::Recipients, &recipients); + + let job = create(Action::SendMsgToSmtp, msg_id.to_u32() as i32, param, 0)?; + + Ok(Some(job)) } -fn load_imap_deletion_msgid(context: &Context) -> sql::Result> { - if let Some(delete_server_after) = context.get_config_delete_server_after() { +#[derive(Debug)] +pub enum Connection<'a> { + Inbox(&'a mut Imap), + Smtp(&'a mut Smtp), +} + +async fn load_imap_deletion_msgid(context: &Context) -> sql::Result> { + if let Some(delete_server_after) = context.get_config_delete_server_after().await { let threshold_timestamp = time() - delete_server_after; - context.sql.query_row_optional( - "SELECT id FROM msgs \ + context + .sql + .query_row_optional( + "SELECT id FROM msgs \ WHERE timestamp < ? \ AND server_uid != 0", - params![threshold_timestamp], - |row| row.get::<_, MsgId>(0), - ) + paramsv![threshold_timestamp], + |row| row.get::<_, MsgId>(0), + ) + .await } else { Ok(None) } } -fn load_imap_deletion_job(context: &Context) -> sql::Result> { - let res = if let Some(msg_id) = load_imap_deletion_msgid(context)? { +async fn load_imap_deletion_job(context: &Context) -> sql::Result> { + let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? { Some(Job::new( Action::DeleteMsgOnImap, msg_id.to_u32(), @@ -1023,188 +854,128 @@ fn load_imap_deletion_job(context: &Context) -> sql::Result> { Ok(res) } -pub fn perform_inbox_jobs(context: &Context) { - info!(context, "dc_perform_inbox_jobs starting.",); - - let probe_imap_network = *context.probe_imap_network.clone().read().unwrap(); - *context.probe_imap_network.write().unwrap() = false; - *context.perform_inbox_jobs_needed.write().unwrap() = false; - - job_perform(context, Thread::Imap, probe_imap_network); - info!(context, "dc_perform_inbox_jobs ended.",); -} - -pub fn perform_mvbox_jobs(context: &Context) { - info!(context, "dc_perform_mbox_jobs EMPTY (for now).",); -} - -pub fn perform_sentbox_jobs(context: &Context) { - info!(context, "dc_perform_sentbox_jobs EMPTY (for now).",); -} - -fn job_perform(context: &Context, thread: Thread, probe_network: bool) { - let mut jobs_loaded = 0; - - while let Some(mut job) = load_next_job(context, thread, probe_network) { - jobs_loaded += 1; - if thread == Thread::Imap && jobs_loaded > 20 { - // Let the fetch run, but return back to the job afterwards. - info!(context, "postponing {}-job {} to run fetch...", thread, job); - *context.perform_inbox_jobs_needed.write().unwrap() = true; - break; - } - - info!(context, "{}-job {} started...", thread, job); - - // some configuration jobs are "exclusive": - // - they are always executed in the imap-thread and the smtp-thread is suspended during execution - // - they may change the database handle; we do not keep old pointers therefore - // - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution - if Action::ConfigureImap == job.action || Action::ImexImap == job.action { - job_kill_action(context, job.action); - context - .sentbox_thread - .clone() - .read() - .unwrap() - .suspend(context); - context - .mvbox_thread - .clone() - .read() - .unwrap() - .suspend(context); - suspend_smtp_thread(context, true); - } - - let try_res = match perform_job_action(context, &mut job, thread, 0) { - Status::RetryNow => perform_job_action(context, &mut job, thread, 1), - x => x, - }; - - if Action::ConfigureImap == job.action || Action::ImexImap == job.action { - context - .sentbox_thread - .clone() - .read() - .unwrap() - .unsuspend(context); - context - .mvbox_thread - .clone() - .read() - .unwrap() - .unsuspend(context); - suspend_smtp_thread(context, false); - break; - } - - match try_res { - Status::RetryNow | Status::RetryLater => { - let tries = job.tries + 1; - - if tries < JOB_RETRIES { - info!( - context, - "{} thread increases job {} tries to {}", thread, job, tries - ); - job.tries = tries; - let time_offset = get_backoff_time_offset(tries); - job.desired_timestamp = time() + time_offset; - info!( - context, - "{}-job #{} not succeeded on try #{}, retry in {} seconds.", - thread, - job.job_id as u32, - tries, - time_offset - ); - job.save(context); - if thread == Thread::Smtp && tries < JOB_RETRIES - 1 { - context - .smtp_state - .clone() - .0 - .lock() - .unwrap() - .perform_jobs_needed = PerformJobsNeeded::AvoidDos; - } - } else { - info!( - context, - "{} thread removes job {} as it exhausted {} retries", - thread, - job, - JOB_RETRIES - ); - if job.action == Action::SendMsgToSmtp { - message::set_msg_failed( - context, - MsgId::new(job.foreign_id), - job.pending_error.as_ref(), - ); - } - job.delete(context); - } - if !probe_network { - continue; - } - // on dc_maybe_network() we stop trying here; - // these jobs are already tried once. - // otherwise, we just continue with the next job - // to give other jobs a chance being tried at least once. - break; - } - Status::Finished(res) => { - if let Err(err) = res { - warn!( - context, - "{} removes job {} as it failed with error {:?}", thread, job, err - ); - } else { - info!(context, "{} removes job {} as it succeeded", thread, job); - } - - job.delete(context); - } +impl<'a> fmt::Display for Connection<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Connection::Inbox(_) => write!(f, "Inbox"), + Connection::Smtp(_) => write!(f, "Smtp"), } } } -fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, tries: u32) -> Status { +impl<'a> Connection<'a> { + fn inbox(&mut self) -> &mut Imap { + match self { + Connection::Inbox(imap) => imap, + _ => panic!("Not an inbox"), + } + } + + fn smtp(&mut self) -> &mut Smtp { + match self { + Connection::Smtp(smtp) => smtp, + _ => panic!("Not a smtp"), + } + } +} + +pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) { + info!(context, "{}-job {} started...", &connection, &job); + + let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await { + Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await, + x => x, + }; + + match try_res { + Status::RetryNow | Status::RetryLater => { + let tries = job.tries + 1; + + if tries < JOB_RETRIES { + info!( + context, + "{} thread increases job {} tries to {}", &connection, job, tries + ); + job.tries = tries; + let time_offset = get_backoff_time_offset(tries); + job.desired_timestamp = time() + time_offset; + info!( + context, + "{}-job #{} not succeeded on try #{}, retry in {} seconds.", + &connection, + job.job_id as u32, + tries, + time_offset + ); + job.save(context).await.unwrap_or_else(|err| { + error!(context, "failed to save job: {}", err); + }); + } else { + info!( + context, + "{} thread removes job {} as it exhausted {} retries", + &connection, + job, + JOB_RETRIES + ); + job.delete(context).await.unwrap_or_else(|err| { + error!(context, "failed to delete job: {}", err); + }); + } + } + Status::Finished(res) => { + if let Err(err) = res { + warn!( + context, + "{} removes job {} as it failed with error {:?}", &connection, job, err + ); + } else { + info!( + context, + "{} removes job {} as it succeeded", &connection, job + ); + } + + job.delete(context).await.unwrap_or_else(|err| { + error!(context, "failed to delete job: {}", err); + }); + } + } +} + +async fn perform_job_action( + context: &Context, + job: &mut Job, + connection: &mut Connection<'_>, + tries: u32, +) -> Status { info!( context, - "{} begin immediate try {} of job {}", thread, tries, job + "{} begin immediate try {} of job {}", &connection, tries, job ); let try_res = match job.action { Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))), - Action::SendMsgToSmtp => job.SendMsgToSmtp(context), - Action::EmptyServer => job.EmptyServer(context), - Action::OldDeleteMsgOnImap => job.DeleteMsgOnImap(context), - Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context), - Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context), - Action::MoveMsg => job.MoveMsg(context), - Action::SendMdn => job.SendMdn(context), - Action::ConfigureImap => JobConfigureImap(context), - Action::ImexImap => match JobImexImap(context, &job) { - Ok(()) => Status::Finished(Ok(())), - Err(err) => { - error!(context, "Import/export failed: {}", err); - Status::Finished(Err(err)) - } - }, - Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job), - Action::MaybeSendLocationsEnded => location::JobMaybeSendLocationsEnded(context, &mut job), + Action::SendMsgToSmtp => job.send_msg_to_smtp(context, connection.smtp()).await, + Action::SendMdn => job.send_mdn(context, connection.smtp()).await, + Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await, + Action::MaybeSendLocationsEnded => { + location::job_maybe_send_locations_ended(context, job).await + } + Action::EmptyServer => job.empty_server(context, connection.inbox()).await, + Action::OldDeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await, + Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await, + Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await, + Action::MoveMsg => job.move_msg(context, connection.inbox()).await, Action::Housekeeping => { - sql::housekeeping(context); + sql::housekeeping(context).await; Status::Finished(Ok(())) } }; info!( context, - "{} finished immediate try {} of job {}", thread, tries, job + "Inbox finished immediate try {} of job {}", tries, job ); try_res @@ -1221,160 +992,166 @@ fn get_backoff_time_offset(tries: u32) -> i64 { seconds as i64 } -fn suspend_smtp_thread(context: &Context, suspend: bool) { - context.smtp_state.0.lock().unwrap().suspended = suspend; - if suspend { - loop { - if !context.smtp_state.0.lock().unwrap().doing_jobs { - return; - } - std::thread::sleep(time::Duration::from_micros(300 * 1000)); - } - } -} - -fn send_mdn(context: &Context, msg: &Message) -> Result<()> { +async fn send_mdn(context: &Context, msg: &Message) -> Result<()> { let mut param = Params::new(); param.set(Param::MsgId, msg.id.to_u32().to_string()); - job_add(context, Action::SendMdn, msg.from_id as i32, param, 0); + add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await; Ok(()) } -fn add_smtp_job( - context: &Context, - action: Action, - msg_id: MsgId, - recipients: Vec, - rendered_msg: &RenderedEmail, -) -> Result<()> { - ensure!(!recipients.is_empty(), "no recipients for smtp job set"); - let mut param = Params::new(); - let bytes = &rendered_msg.message; - let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?; +/// Creates a job. +pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result { + ensure!( + action != Action::Unknown, + "Invalid action passed to job_add" + ); - let recipients = recipients.join("\x1e"); - param.set(Param::File, blob.as_name()); - param.set(Param::Recipients, &recipients); - - job_add(context, action, msg_id.to_u32() as i32, param, 0); - - Ok(()) + Ok(Job::new(action, foreign_id as u32, param, delay_seconds)) } -/// Adds a job to the database, scheduling it `delay_seconds` -/// after the current time. -pub fn job_add( - context: &Context, - action: Action, - foreign_id: i32, - param: Params, - delay_seconds: i64, -) { - if action == Action::Unknown { - error!(context, "Invalid action passed to job_add"); - return; - } - - let job = Job::new(action, foreign_id as u32, param, delay_seconds); - job.save(context); +/// Adds a job to the database, scheduling it. +pub async fn add(context: &Context, job: Job) { + let action = job.action; + let delay_seconds = job.delay_seconds(); + job.save(context).await.unwrap_or_else(|err| { + error!(context, "failed to save job: {}", err); + }); if delay_seconds == 0 { - let thread: Thread = action.into(); - match thread { - Thread::Imap => interrupt_inbox_idle(context), - Thread::Smtp => interrupt_smtp_idle(context), - Thread::Unknown => {} + match action { + Action::Unknown => unreachable!(), + Action::Housekeeping + | Action::EmptyServer + | Action::OldDeleteMsgOnImap + | Action::DeleteMsgOnImap + | Action::MarkseenMsgOnImap + | Action::MoveMsg => { + info!(context, "interrupt: imap"); + context.interrupt_inbox(false).await; + } + Action::MaybeSendLocations + | Action::MaybeSendLocationsEnded + | Action::SendMdn + | Action::SendMsgToSmtp => { + info!(context, "interrupt: smtp"); + context.interrupt_smtp(false).await; + } } } } -pub fn interrupt_smtp_idle(context: &Context) { - info!(context, "Interrupting SMTP-idle...",); - - let &(ref lock, ref cvar) = &*context.smtp_state.clone(); - let mut state = lock.lock().unwrap(); - - state.perform_jobs_needed = PerformJobsNeeded::AtOnce; - state.idle = true; - cvar.notify_one(); - info!(context, "Interrupting SMTP-idle... ended",); -} - /// Load jobs from the database. /// /// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load /// IMAP jobs. The `probe_network` parameter decides how to query /// jobs, this is tricky and probably wrong currently. Look at the /// SQL queries for details. -fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option { +pub(crate) async fn load_next( + context: &Context, + thread: Thread, + probe_network: bool, +) -> Option { + info!(context, "loading job for {}-thread", thread); let query = if !probe_network { // processing for first-try and after backoff-timeouts: // process jobs in the order they were added. - "SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries \ - FROM jobs WHERE thread=? AND desired_timestamp<=? ORDER BY action DESC, added_timestamp;" + 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; +"# } else { // processing after call to dc_maybe_network(): // process _all_ pending jobs that failed before // in the order of their backoff-times. - "SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries \ - FROM jobs WHERE thread=? AND tries>0 ORDER BY desired_timestamp, action DESC;" + 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; +"# }; - let params_no_probe = params![thread as i64, time()]; - let params_probe = params![thread as i64]; - let params: &[&dyn rusqlite::ToSql] = if !probe_network { - params_no_probe + let thread_i = thread as i64; + let t = time(); + let params = if !probe_network { + paramsv![thread_i, t] } else { - params_probe + paramsv![thread_i] }; - let job = context - .sql - .query_map( - query, - params, - |row| { + let job = loop { + let job_res = context + .sql + .query_row_optional(query, params.clone(), |row| { let job = Job { - job_id: row.get(0)?, - action: row.get(1)?, - foreign_id: row.get(2)?, - desired_timestamp: row.get(5)?, - added_timestamp: row.get(4)?, - tries: row.get(6)?, - param: row.get::<_, String>(3)?.parse().unwrap_or_default(), + 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) - }, - |jobs| { - for job in jobs { - match job { - Ok(j) => return Ok(Some(j)), - Err(e) => warn!(context, "Bad job from the database: {}", e), + }) + .await; + + match job_res { + Ok(job) => break job, + Err(err) => { + // Remove invalid job from the DB + info!(context, "cleaning up job, because of {}", err); + + // TODO: improve by only doing a single query + match context + .sql + .query_row(query, params.clone(), |row| row.get::<_, i32>(0)) + .await + { + Ok(id) => { + context + .sql + .execute("DELETE FROM jobs WHERE id=?;", paramsv![id]) + .await + .ok(); + } + Err(err) => { + error!(context, "failed to retrieve invalid job from DB: {}", err); + break None; } } - Ok(None) - }, - ) - .unwrap_or_default(); - - if thread == Thread::Imap { - if let Some(job) = job { - if job.action < Action::DeleteMsgOnImap { - load_imap_deletion_job(context) - .unwrap_or_default() - .or(Some(job)) - } else { - Some(job) } - } else { - load_imap_deletion_job(context).unwrap_or_default() } - } else { - job + }; + + match thread { + Thread::Unknown => { + error!(context, "unknown thread for job"); + None + } + Thread::Imap => { + if let Some(job) = job { + if job.action < Action::DeleteMsgOnImap { + load_imap_deletion_job(context) + .await + .unwrap_or_default() + .or(Some(job)) + } else { + Some(job) + } + } else { + load_imap_deletion_job(context).await.unwrap_or_default() + } + } + Thread::Smtp => job, } } @@ -1384,7 +1161,7 @@ mod tests { use crate::test_utils::*; - fn insert_job(context: &Context, foreign_id: i64) { + async fn insert_job(context: &Context, foreign_id: i64) { let now = time(); context .sql @@ -1392,7 +1169,7 @@ mod tests { "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?, ?, ?, ?, ?, ?);", - params![ + paramsv![ now, Thread::from(Action::MoveMsg), Action::MoveMsg, @@ -1401,21 +1178,32 @@ mod tests { now ], ) + .await .unwrap(); } - #[test] - fn test_load_next_job() { + #[async_std::test] + async fn test_load_next_job_two() { // We want to ensure that loading jobs skips over jobs which // fails to load from the database instead of failing to load // all jobs. - let t = dummy_context(); - insert_job(&t.ctx, -1); // This can not be loaded into Job struct. - let jobs = load_next_job(&t.ctx, Thread::from(Action::MoveMsg), false); + let t = dummy_context().await; + insert_job(&t.ctx, -1).await; // This can not be loaded into Job struct. + let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await; assert!(jobs.is_none()); - insert_job(&t.ctx, 1); - let jobs = load_next_job(&t.ctx, Thread::from(Action::MoveMsg), false); + insert_job(&t.ctx, 1).await; + let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await; + assert!(jobs.is_some()); + } + + #[async_std::test] + async fn test_load_next_job_one() { + let t = dummy_context().await; + + insert_job(&t.ctx, 1).await; + + let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await; assert!(jobs.is_some()); } } diff --git a/src/job_thread.rs b/src/job_thread.rs deleted file mode 100644 index 12512a3d6..000000000 --- a/src/job_thread.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::sync::{Arc, Condvar, Mutex}; - -use crate::context::Context; -use crate::error::{format_err, Result}; -use crate::imap::Imap; - -#[derive(Debug)] -pub struct JobThread { - pub name: &'static str, - pub folder_config_name: &'static str, - pub imap: Imap, - pub state: Arc<(Mutex, Condvar)>, -} - -#[derive(Clone, Debug, Default)] -pub struct JobState { - idle: bool, - jobs_needed: bool, - suspended: bool, - using_handle: bool, -} - -impl JobThread { - pub fn new(name: &'static str, folder_config_name: &'static str, imap: Imap) -> Self { - JobThread { - name, - folder_config_name, - imap, - state: Arc::new((Mutex::new(Default::default()), Condvar::new())), - } - } - - pub fn suspend(&self, context: &Context) { - info!(context, "Suspending {}-thread.", self.name,); - { - self.state.0.lock().unwrap().suspended = true; - } - self.interrupt_idle(context); - loop { - let using_handle = self.state.0.lock().unwrap().using_handle; - if !using_handle { - return; - } - std::thread::sleep(std::time::Duration::from_micros(300 * 1000)); - } - } - - pub fn unsuspend(&self, context: &Context) { - info!(context, "Unsuspending {}-thread.", self.name); - - let &(ref lock, ref cvar) = &*self.state.clone(); - let mut state = lock.lock().unwrap(); - - state.suspended = false; - state.idle = true; - cvar.notify_one(); - } - - pub fn interrupt_idle(&self, context: &Context) { - { - self.state.0.lock().unwrap().jobs_needed = true; - } - - info!(context, "Interrupting {}-IDLE...", self.name); - - self.imap.interrupt_idle(context); - - let &(ref lock, ref cvar) = &*self.state.clone(); - let mut state = lock.lock().unwrap(); - - state.idle = true; - cvar.notify_one(); - info!(context, "Interrupting {}-IDLE... finished", self.name); - } - - pub async fn fetch(&mut self, context: &Context, use_network: bool) { - { - let &(ref lock, _) = &*self.state.clone(); - let mut state = lock.lock().unwrap(); - - if state.suspended { - return; - } - - state.using_handle = true; - } - - if use_network { - if let Err(err) = self.connect_and_fetch(context).await { - warn!(context, "connect+fetch failed: {}, reconnect & retry", err); - self.imap.trigger_reconnect(); - if let Err(err) = self.connect_and_fetch(context).await { - warn!(context, "connect+fetch failed: {}", err); - } - } - } - self.state.0.lock().unwrap().using_handle = false; - } - - async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> { - let prefix = format!("{}-fetch", self.name); - self.imap.connect_configured(context)?; - if let Some(watch_folder) = self.get_watch_folder(context) { - let start = std::time::Instant::now(); - info!(context, "{} started...", prefix); - let res = self - .imap - .fetch(context, &watch_folder) - .await - .map_err(Into::into); - let elapsed = start.elapsed().as_millis(); - info!(context, "{} done in {:.3} ms.", prefix, elapsed); - - res - } else { - Err(format_err!("WatchFolder not found: not-set")) - } - } - - fn get_watch_folder(&self, context: &Context) -> Option { - match context.sql.get_raw_config(context, self.folder_config_name) { - Some(name) => Some(name), - None => { - if self.folder_config_name == "configured_inbox_folder" { - // initialized with old version, so has not set configured_inbox_folder - Some("INBOX".to_string()) - } else { - None - } - } - } - } - - pub fn idle(&self, context: &Context, use_network: bool) { - { - let &(ref lock, ref cvar) = &*self.state.clone(); - let mut state = lock.lock().unwrap(); - - if state.jobs_needed { - info!( - context, - "{}-IDLE will not be started as it was interrupted while not idling.", - self.name, - ); - state.jobs_needed = false; - return; - } - - if state.suspended { - while !state.idle { - state = cvar.wait(state).unwrap(); - } - state.idle = false; - return; - } - - state.using_handle = true; - - if !use_network { - state.using_handle = false; - - while !state.idle { - state = cvar.wait(state).unwrap(); - } - state.idle = false; - return; - } - } - - let prefix = format!("{}-IDLE", self.name); - let do_fake_idle = match self.imap.connect_configured(context) { - Ok(()) => { - if !self.imap.can_idle() { - true // we have to do fake_idle - } else { - let watch_folder = self.get_watch_folder(context); - info!(context, "{} started...", prefix); - let res = self.imap.idle(context, watch_folder); - info!(context, "{} ended...", prefix); - if let Err(err) = res { - warn!(context, "{} failed: {} -> reconnecting", prefix, err); - // something is borked, let's start afresh on the next occassion - self.imap.disconnect(context); - } - false - } - } - Err(err) => { - info!(context, "{}-IDLE connection fail: {:?}", self.name, err); - // if the connection fails, use fake_idle to retry periodically - // fake_idle() will be woken up by interrupt_idle() as - // well so will act on maybe_network events - true - } - }; - if do_fake_idle { - let watch_folder = self.get_watch_folder(context); - self.imap.fake_idle(context, watch_folder); - } - - self.state.0.lock().unwrap().using_handle = false; - } -} diff --git a/src/key.rs b/src/key.rs index 15609c27d..035507b5e 100644 --- a/src/key.rs +++ b/src/key.rs @@ -2,8 +2,9 @@ use std::collections::BTreeMap; use std::io::Cursor; -use std::path::Path; +use async_std::path::Path; +use async_trait::async_trait; use num_traits::FromPrimitive; use pgp::composed::Deserializable; use pgp::ser::Serialize; @@ -37,6 +38,8 @@ pub enum Error { NoConfiguredAddr, #[error("Configured address is invalid: {}", _0)] InvalidConfiguredAddr(#[from] InvalidEmailError), + #[error("no data provided")] + Empty, } pub type Result = std::result::Result; @@ -46,6 +49,7 @@ pub type Result = std::result::Result; /// This trait is implemented for rPGP's [SignedPublicKey] and /// [SignedSecretKey] types and makes working with them a little /// easier in the deltachat world. +#[async_trait] pub trait DcKey: Serialize + Deserializable { type KeyType: Serialize + Deserializable; @@ -65,7 +69,7 @@ pub trait DcKey: Serialize + Deserializable { } /// Load the users' default key from the database. - fn load_self(context: &Context) -> Result; + async fn load_self(context: &Context) -> Result; /// Serialise the key to a base64 string. fn to_base64(&self) -> String { @@ -79,23 +83,28 @@ pub trait DcKey: Serialize + Deserializable { } } +#[async_trait] impl DcKey for SignedPublicKey { type KeyType = SignedPublicKey; - fn load_self(context: &Context) -> Result { - match context.sql.query_row( - r#" + async fn load_self(context: &Context) -> Result { + match context + .sql + .query_row( + r#" SELECT public_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - params![], - |row| row.get::<_, Vec>(0), - ) { + paramsv![], + |row| row.get::<_, Vec>(0), + ) + .await + { Ok(bytes) => Self::from_slice(&bytes), Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { - let keypair = generate_keypair(context)?; + let keypair = generate_keypair(context).await?; Ok(keypair.public) } Err(err) => Err(err.into()), @@ -103,23 +112,28 @@ impl DcKey for SignedPublicKey { } } +#[async_trait] impl DcKey for SignedSecretKey { type KeyType = SignedSecretKey; - fn load_self(context: &Context) -> Result { - match context.sql.query_row( - r#" + async fn load_self(context: &Context) -> Result { + match context + .sql + .query_row( + r#" SELECT private_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - params![], - |row| row.get::<_, Vec>(0), - ) { + paramsv![], + |row| row.get::<_, Vec>(0), + ) + .await + { Ok(bytes) => Self::from_slice(&bytes), Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { - let keypair = generate_keypair(context)?; + let keypair = generate_keypair(context).await?; Ok(keypair.secret) } Err(err) => Err(err.into()), @@ -127,24 +141,29 @@ impl DcKey for SignedSecretKey { } } -fn generate_keypair(context: &Context) -> Result { +async fn generate_keypair(context: &Context) -> Result { let addr = context .get_config(Config::ConfiguredAddr) + .await .ok_or_else(|| Error::NoConfiguredAddr)?; let addr = EmailAddress::new(&addr)?; - let _guard = context.generating_key_mutex.lock().unwrap(); + let _guard = context.generating_key_mutex.lock().await; // Check if the key appeared while we were waiting on the lock. - match context.sql.query_row( - r#" + match context + .sql + .query_row( + r#" SELECT public_key, private_key FROM keypairs WHERE addr=?1 AND is_default=1; "#, - params![addr], - |row| Ok((row.get::<_, Vec>(0)?, row.get::<_, Vec>(1)?)), - ) { + paramsv![addr], + |row| Ok((row.get::<_, Vec>(0)?, row.get::<_, Vec>(1)?)), + ) + .await + { Ok((pub_bytes, sec_bytes)) => Ok(KeyPair { addr, public: SignedPublicKey::from_slice(&pub_bytes)?, @@ -152,11 +171,13 @@ fn generate_keypair(context: &Context) -> Result { }), Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { let start = std::time::Instant::now(); - let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)) + let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await) .unwrap_or_default(); info!(context, "Generating keypair with type {}", keytype); - let keypair = crate::pgp::create_keypair(addr, keytype)?; - store_self_keypair(context, &keypair, KeyPairUse::Default)?; + let keypair = + async_std::task::spawn_blocking(move || crate::pgp::create_keypair(addr, keytype)) + .await?; + store_self_keypair(context, &keypair, KeyPairUse::Default).await?; info!( context, "Keypair generated in {:.3}s.", @@ -243,22 +264,17 @@ impl Key { !self.is_public() } - pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Option { + pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Result { if bytes.is_empty() { - return None; + return Err(Error::Empty); } - let res: std::result::Result = match key_type { - KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into), - KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into), + + let res = match key_type { + KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes))?.into(), + KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes))?.into(), }; - match res { - Ok(key) => Some(key), - Err(err) => { - eprintln!("Invalid key bytes: {:?}", err); - None - } - } + Ok(res) } pub fn from_armored_string( @@ -323,14 +339,14 @@ impl Key { .expect("failed to serialize key") } - pub fn write_asc_to_file( + pub async fn write_asc_to_file( &self, file: impl AsRef, context: &Context, ) -> std::io::Result<()> { let file_content = self.to_asc(None).into_bytes(); - let res = dc_write_file(context, &file, &file_content); + let res = dc_write_file(context, &file, &file_content).await; if res.is_err() { error!(context, "Cannot write key to {}", file.as_ref().display()); } @@ -402,7 +418,7 @@ impl SaveKeyError { /// same key again overwrites it. /// /// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr -pub fn store_self_keypair( +pub async fn store_self_keypair( context: &Context, keypair: &KeyPair, default: KeyPairUse, @@ -421,34 +437,37 @@ pub fn store_self_keypair( .sql .execute( "DELETE FROM keypairs WHERE public_key=? OR private_key=?;", - params![public_key, secret_key], + paramsv![public_key, secret_key], ) + .await .map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?; if default == KeyPairUse::Default { context .sql - .execute("UPDATE keypairs SET is_default=0;", params![]) + .execute("UPDATE keypairs SET is_default=0;", paramsv![]) + .await .map_err(|err| SaveKeyError::new("failed to clear default", err))?; } let is_default = match default { - KeyPairUse::Default => true, - KeyPairUse::ReadOnly => false, + KeyPairUse::Default => true as i32, + KeyPairUse::ReadOnly => false as i32, }; + + let addr = keypair.addr.to_string(); + let t = time(); + + let params = paramsv![addr, is_default, public_key, secret_key, t]; context .sql .execute( "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);", - params![ - keypair.addr.to_string(), - is_default as i32, - public_key, - secret_key, - time() - ], + params, ) - .map(|_| ()) - .map_err(|err| SaveKeyError::new("failed to insert keypair", err)) + .await + .map_err(|err| SaveKeyError::new("failed to insert keypair", err))?; + + Ok(()) } /// Make a fingerprint human-readable, in hex format. @@ -483,6 +502,7 @@ mod tests { use crate::test_utils::*; use std::convert::TryFrom; + use async_std::sync::Arc; use lazy_static::lazy_static; lazy_static! { @@ -604,58 +624,62 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD KeyType::Private }, ); - assert!(bad_key.is_none()); + assert!(bad_key.is_err()); } } - #[test] - fn test_load_self_existing() { + #[async_std::test] + async fn test_load_self_existing() { let alice = alice_keypair(); - let t = dummy_context(); - configure_alice_keypair(&t.ctx); - let pubkey = SignedPublicKey::load_self(&t.ctx).unwrap(); + let t = dummy_context().await; + configure_alice_keypair(&t.ctx).await; + let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap(); assert_eq!(alice.public, pubkey); - let seckey = SignedSecretKey::load_self(&t.ctx).unwrap(); + let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap(); assert_eq!(alice.secret, seckey); } - #[test] + #[async_std::test] #[ignore] // generating keys is expensive - fn test_load_self_generate_public() { - let t = dummy_context(); + async fn test_load_self_generate_public() { + let t = dummy_context().await; t.ctx .set_config(Config::ConfiguredAddr, Some("alice@example.com")) + .await .unwrap(); - let key = SignedPublicKey::load_self(&t.ctx); + let key = SignedPublicKey::load_self(&t.ctx).await; assert!(key.is_ok()); } - #[test] + #[async_std::test] #[ignore] // generating keys is expensive - fn test_load_self_generate_secret() { - let t = dummy_context(); + async fn test_load_self_generate_secret() { + let t = dummy_context().await; t.ctx .set_config(Config::ConfiguredAddr, Some("alice@example.com")) + .await .unwrap(); - let key = SignedSecretKey::load_self(&t.ctx); + let key = SignedSecretKey::load_self(&t.ctx).await; assert!(key.is_ok()); } - #[test] + #[async_std::test] #[ignore] // generating keys is expensive - fn test_load_self_generate_concurrent() { - use std::sync::Arc; + async fn test_load_self_generate_concurrent() { use std::thread; - let t = dummy_context(); + let t = dummy_context().await; t.ctx .set_config(Config::ConfiguredAddr, Some("alice@example.com")) + .await .unwrap(); - let ctx = Arc::new(t.ctx); - let ctx0 = Arc::clone(&ctx); - let thr0 = thread::spawn(move || SignedPublicKey::load_self(&ctx0)); - let ctx1 = Arc::clone(&ctx); - let thr1 = thread::spawn(move || SignedPublicKey::load_self(&ctx1)); + let ctx = t.ctx.clone(); + let ctx0 = ctx.clone(); + let thr0 = + thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0))); + let ctx1 = ctx.clone(); + let thr1 = + thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1))); let res0 = thr0.join().unwrap(); let res1 = thr1.join().unwrap(); assert_eq!(res0.unwrap(), res1.unwrap()); @@ -686,22 +710,29 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD assert_eq!(public.primary_key, KEYPAIR.public.primary_key); } - #[test] - fn test_save_self_key_twice() { + #[async_std::test] + async fn test_save_self_key_twice() { // Saving the same key twice should result in only one row in // the keypairs table. - let t = dummy_context(); - let nrows = || { - t.ctx - .sql - .query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![]) + let t = dummy_context().await; + let ctx = Arc::new(t.ctx); + + let ctx1 = ctx.clone(); + let nrows = || async { + ctx1.sql + .query_get_value::(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![]) + .await .unwrap() }; - assert_eq!(nrows(), 0); - store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap(); - assert_eq!(nrows(), 1); - store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap(); - assert_eq!(nrows(), 1); + assert_eq!(nrows().await, 0); + store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default) + .await + .unwrap(); + assert_eq!(nrows().await, 1); + store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default) + .await + .unwrap(); + assert_eq!(nrows().await, 1); } // Convenient way to create a new key if you need one, run with diff --git a/src/keyring.rs b/src/keyring.rs index 25d197cd3..c962e3fbd 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -1,45 +1,47 @@ -use std::borrow::Cow; +use anyhow::Result; use crate::constants::KeyType; use crate::context::Context; use crate::key::Key; -use crate::sql::Sql; #[derive(Default, Clone, Debug)] -pub struct Keyring<'a> { - keys: Vec>, +pub struct Keyring { + keys: Vec, } -impl<'a> Keyring<'a> { - pub fn add_owned(&mut self, key: Key) { - self.add(Cow::Owned(key)) +impl Keyring { + pub fn add(&mut self, key: Key) { + self.keys.push(key) } - pub fn add_ref(&mut self, key: &'a Key) { - self.add(Cow::Borrowed(key)) + pub fn len(&self) -> usize { + self.keys.len() } - fn add(&mut self, key: Cow<'a, Key>) { - self.keys.push(key); + pub fn is_empty(&self) -> bool { + self.keys.is_empty() } - pub fn keys(&self) -> &[Cow<'a, Key>] { + pub fn keys(&self) -> &[Key] { &self.keys } - pub fn load_self_private_for_decrypting( - &mut self, + pub async fn load_self_private_for_decrypting( context: &Context, self_addr: impl AsRef, - sql: &Sql, - ) -> bool { - sql.query_get_value( - context, - "SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;", - &[self_addr.as_ref()], - ) - .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Private)) - .map(|key| self.add_owned(key)) - .is_some() + ) -> Result { + let blob: Vec = context + .sql + .query_get_value_result( + "SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;", + paramsv![self_addr.as_ref().to_string()], + ) + .await? + .unwrap_or_default(); + + let key = async_std::task::spawn_blocking(move || Key::from_slice(&blob, KeyType::Private)) + .await?; + + Ok(Self { keys: vec![key] }) } } diff --git a/src/lib.rs b/src/lib.rs index 511a6a4c1..98e40d7db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,11 +14,22 @@ extern crate strum_macros; #[macro_use] extern crate debug_stub_derive; +pub trait ToSql: rusqlite::ToSql + Send + Sync {} + +impl ToSql for T {} + #[macro_use] pub mod log; #[macro_use] pub mod error; +#[cfg(feature = "internals")] +#[macro_use] +pub mod sql; +#[cfg(not(feature = "internals"))] +#[macro_use] +mod sql; + pub mod headerdef; pub(crate) mod events; @@ -36,9 +47,9 @@ pub mod context; mod e2ee; mod imap; pub mod imex; +mod scheduler; #[macro_use] pub mod job; -mod job_thread; pub mod key; mod keyring; pub mod location; @@ -56,7 +67,6 @@ pub mod qr; pub mod securejoin; mod simplify; mod smtp; -pub mod sql; pub mod stock; mod token; #[macro_use] diff --git a/src/location.rs b/src/location.rs index c77396b62..c27f90c41 100644 --- a/src/location.rs +++ b/src/location.rs @@ -10,11 +10,10 @@ use crate::context::*; use crate::dc_tools::*; use crate::error::{ensure, Error}; use crate::events::Event; -use crate::job::{self, job_action_exists, job_add, Job}; +use crate::job::{self, Job}; use crate::message::{Message, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::*; -use crate::sql; use crate::stock::StockMessage; /// Location record @@ -191,91 +190,103 @@ impl Kml { } // location streaming -pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) { +pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) { let now = time(); if !(seconds < 0 || chat_id.is_special()) { - let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id); - if sql::execute( - context, - &context.sql, - "UPDATE chats \ + let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id).await; + if context + .sql + .execute( + "UPDATE chats \ SET locations_send_begin=?, \ locations_send_until=? \ WHERE id=?", - params![ - if 0 != seconds { now } else { 0 }, - if 0 != seconds { now + seconds } else { 0 }, - chat_id, - ], - ) - .is_ok() + paramsv![ + if 0 != seconds { now } else { 0 }, + if 0 != seconds { now + seconds } else { 0 }, + chat_id, + ], + ) + .await + .is_ok() { if 0 != seconds && !is_sending_locations_before { let mut msg = Message::new(Viewtype::Text); - msg.text = - Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); - msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); - chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); - } else if 0 == seconds && is_sending_locations_before { - let stock_str = - context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); - chat::add_info_msg(context, chat_id, stock_str); - } - context.call_cb(Event::ChatModified(chat_id)); - if 0 != seconds { - schedule_MAYBE_SEND_LOCATIONS(context, false); - job_add( - context, - job::Action::MaybeSendLocationsEnded, - chat_id.to_u32() as i32, - Params::new(), - seconds + 1, + msg.text = Some( + context + .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0) + .await, ); + msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); + chat::send_msg(context, chat_id, &mut msg) + .await + .unwrap_or_default(); + } else if 0 == seconds && is_sending_locations_before { + let stock_str = context + .stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0) + .await; + chat::add_info_msg(context, chat_id, stock_str).await; + } + context.emit_event(Event::ChatModified(chat_id)); + if 0 != seconds { + schedule_maybe_send_locations(context, false).await; + job::add( + context, + job::Job::new( + job::Action::MaybeSendLocationsEnded, + chat_id.to_u32(), + Params::new(), + seconds + 1, + ), + ) + .await; } } } } -#[allow(non_snake_case)] -fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) { - if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) { - job_add( +async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) { + if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await { + job::add( context, - job::Action::MaybeSendLocations, - 0, - Params::new(), - 60, - ); + job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60), + ) + .await; }; } -pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool { +pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool { context .sql .exists( "SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;", - params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()], + paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()], ) + .await .unwrap_or_default() } -pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { +pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { if latitude == 0.0 && longitude == 0.0 { return true; } let mut continue_streaming = false; - if let Ok(chats) = context.sql.query_map( - "SELECT id FROM chats WHERE locations_send_until>?;", - params![time()], - |row| row.get::<_, i32>(0), - |chats| chats.collect::, _>>().map_err(Into::into), - ) { + if let Ok(chats) = context + .sql + .query_map( + "SELECT id FROM chats WHERE locations_send_until>?;", + paramsv![time()], + |row| row.get::<_, i32>(0), + |chats| chats.collect::, _>>().map_err(Into::into), + ) + .await + { for chat_id in chats { if let Err(err) = context.sql.execute( "INSERT INTO locations \ (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", - params![ + paramsv![ latitude, longitude, accuracy, @@ -283,22 +294,22 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b chat_id, DC_CONTACT_ID_SELF, ] - ) { + ).await { warn!(context, "failed to store location {:?}", err); } else { continue_streaming = true; } } if continue_streaming { - context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); + context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); }; - schedule_MAYBE_SEND_LOCATIONS(context, false); + schedule_maybe_send_locations(context, false).await; } continue_streaming } -pub fn get_range( +pub async fn get_range( context: &Context, chat_id: ChatId, contact_id: u32, @@ -317,7 +328,7 @@ pub fn get_range( AND (? OR l.from_id=?) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", - params![ + paramsv![ if chat_id.is_unset() { 1 } else { 0 }, chat_id, if contact_id == 0 { 1 } else { 0 }, @@ -356,6 +367,7 @@ pub fn get_range( Ok(ret) }, ) + .await .unwrap_or_default() } @@ -364,28 +376,33 @@ fn is_marker(txt: &str) -> bool { } /// Deletes all locations from the database. -pub fn delete_all(context: &Context) -> Result<(), Error> { - sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?; - context.call_cb(Event::LocationChanged(None)); +pub async fn delete_all(context: &Context) -> Result<(), Error> { + context + .sql + .execute("DELETE FROM locations;", paramsv![]) + .await?; + context.emit_event(Event::LocationChanged(None)); Ok(()) } -pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> { +pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> { let mut last_added_location_id = 0; let self_addr = context .get_config(Config::ConfiguredAddr) + .await .unwrap_or_default(); let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", - params![chat_id], |row| { + 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)?; Ok((send_begin, send_until, last_sent)) - })?; + }) + .await?; let now = time(); let mut location_count = 0; @@ -404,7 +421,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro AND independent=0 \ GROUP BY timestamp \ ORDER BY timestamp;", - params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], + 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)?; @@ -429,7 +446,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro } Ok(()) } - )?; + ).await?; ret += "\n"; } @@ -462,37 +479,38 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String ) } -pub fn set_kml_sent_timestamp( +pub async fn set_kml_sent_timestamp( context: &Context, chat_id: ChatId, timestamp: i64, ) -> Result<(), Error> { - sql::execute( - context, - &context.sql, - "UPDATE chats SET locations_last_sent=? WHERE id=?;", - params![timestamp, chat_id], - )?; - + context + .sql + .execute( + "UPDATE chats SET locations_last_sent=? WHERE id=?;", + paramsv![timestamp, chat_id], + ) + .await?; Ok(()) } -pub fn set_msg_location_id( +pub async fn set_msg_location_id( context: &Context, msg_id: MsgId, location_id: u32, ) -> Result<(), Error> { - sql::execute( - context, - &context.sql, - "UPDATE msgs SET location_id=? WHERE id=?;", - params![location_id, msg_id], - )?; + context + .sql + .execute( + "UPDATE msgs SET location_id=? WHERE id=?;", + paramsv![location_id, msg_id], + ) + .await?; Ok(()) } -pub fn save( +pub async fn save( context: &Context, chat_id: ChatId, contact_id: u32, @@ -500,54 +518,66 @@ pub fn save( independent: bool, ) -> Result { ensure!(!chat_id.is_special(), "Invalid chat id"); - context - .sql - .prepare2( - "SELECT id FROM locations WHERE timestamp=? AND from_id=?", - "INSERT INTO locations\ + + let mut newest_timestamp = 0; + let mut newest_location_id = 0; + + for location in locations { + let &Location { + timestamp, + latitude, + longitude, + accuracy, + .. + } = location; + context + .sql + .with_conn(move |mut conn| { + let mut stmt_test = conn + .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?; + let mut stmt_insert = conn.prepare_cached( + "INSERT INTO locations\ (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ VALUES (?,?,?,?,?,?,?);", - |mut stmt_test, mut stmt_insert, conn| { - let mut newest_timestamp = 0; - let mut newest_location_id = 0; + )?; - for location in locations { - let exists = - stmt_test.exists(params![location.timestamp, contact_id as i32])?; + let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?; - if independent || !exists { - stmt_insert.execute(params![ - location.timestamp, + 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, - chat_id, - location.latitude, - location.longitude, - location.accuracy, - independent, - ])?; - - if location.timestamp > newest_timestamp { - newest_timestamp = location.timestamp; - newest_location_id = sql::get_rowid2_with_conn( - context, - conn, - "locations", - "timestamp", - location.timestamp, - "from_id", - contact_id as i32, - ); - } + )?; } } - Ok(newest_location_id) - }, - ) - .map_err(Into::into) + Ok(()) + }) + .await?; + } + + Ok(newest_location_id) } -#[allow(non_snake_case)] -pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status { +pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status { let now = time(); let mut continue_streaming = false; info!( @@ -555,101 +585,118 @@ pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Statu " ----------------- MAYBE_SEND_LOCATIONS -------------- ", ); - if let Ok(rows) = context.sql.query_map( - "SELECT id, locations_send_begin, locations_last_sent \ + let rows = context + .sql + .query_map( + "SELECT id, locations_send_begin, locations_last_sent \ FROM chats \ WHERE locations_send_until>?;", - params![now], - |row| { - let chat_id: ChatId = row.get(0)?; - let locations_send_begin: i64 = row.get(1)?; - let locations_last_sent: i64 = row.get(2)?; - continue_streaming = true; + paramsv![now], + |row| { + let chat_id: ChatId = row.get(0)?; + let locations_send_begin: i64 = row.get(1)?; + let locations_last_sent: i64 = row.get(2)?; + continue_streaming = true; - // be a bit tolerant as the timer may not align exactly with time(NULL) - if now - locations_last_sent < (60 - 3) { - Ok(None) - } else { - Ok(Some((chat_id, locations_send_begin, locations_last_sent))) - } - }, - |rows| { - rows.filter_map(|v| v.transpose()) - .collect::, _>>() - .map_err(Into::into) - }, - ) { + // be a bit tolerant as the timer may not align exactly with time(NULL) + if now - locations_last_sent < (60 - 3) { + Ok(None) + } else { + Ok(Some((chat_id, locations_send_begin, locations_last_sent))) + } + }, + |rows| { + rows.filter_map(|v| v.transpose()) + .collect::, _>>() + .map_err(Into::into) + }, + ) + .await; + + if rows.is_ok() { let msgs = context .sql - .prepare( - "SELECT id \ + .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;", - |mut stmt_locations, _| { - let msgs = rows - .into_iter() - .filter_map(|(chat_id, locations_send_begin, locations_last_sent)| { - if !stmt_locations - .exists(params![ - DC_CONTACT_ID_SELF, - locations_send_begin, - locations_last_sent, - ]) - .unwrap_or_default() - { - // if there is no new location, there's nothing to send. - // however, maybe we want to bypass this test eg. 15 minutes - None - } else { - // pending locations are attached automatically to every message, - // so also to this empty text message. - // DC_CMD_LOCATION is only needed to create a nicer subject. - // - // for optimisation and to avoid flooding the sending queue, - // we could sending these messages only if we're really online. - // the easiest way to determine this, is to check for an empty message queue. - // (might not be 100%, however, as positions are sent combined later - // and dc_set_location() is typically called periodically, this is ok) - let mut msg = Message::new(Viewtype::Text); - msg.hidden = true; - msg.param.set_cmd(SystemMessage::LocationOnly); - Some((chat_id, msg)) - } - }) - .collect::>(); - Ok(msgs) - }, - ) - .unwrap_or_default(); // TODO: Better error handling + )?; + + 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(); for (chat_id, mut msg) in msgs.into_iter() { // TODO: better error handling - chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); + chat::send_msg(context, chat_id, &mut msg) + .await + .unwrap_or_default(); } } + if continue_streaming { - schedule_MAYBE_SEND_LOCATIONS(context, true); + schedule_maybe_send_locations(context, true).await; } job::Status::Finished(Ok(())) } -#[allow(non_snake_case)] -pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status { +pub(crate) async fn job_maybe_send_locations_ended( + context: &Context, + job: &mut Job, +) -> job::Status { // this function is called when location-streaming _might_ have ended for a chat. // the function checks, if location-streaming is really ended; // if so, a device-message is added if not yet done. let chat_id = ChatId::new(job.foreign_id); - let (send_begin, send_until) = job_try!(context.sql.query_row( - "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", - params![chat_id], - |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), - )); + let (send_begin, send_until) = job_try!( + context + .sql + .query_row( + "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", + paramsv![chat_id], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), + ) + .await + ); if !(send_begin != 0 && time() <= send_until) { // still streaming - @@ -659,12 +706,14 @@ pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> jo // not streaming, device-message already sent job_try!(context.sql.execute( "UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?", - params![chat_id], - )); + paramsv![chat_id], + ).await); - let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); - chat::add_info_msg(context, chat_id, stock_str); - context.call_cb(Event::ChatModified(chat_id)); + let stock_str = context + .stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0) + .await; + chat::add_info_msg(context, chat_id, stock_str).await; + context.emit_event(Event::ChatModified(chat_id)); } } job::Status::Finished(Ok(())) @@ -675,9 +724,9 @@ mod tests { use super::*; use crate::test_utils::dummy_context; - #[test] - fn test_kml_parse() { - let context = dummy_context(); + #[async_std::test] + async fn test_kml_parse() { + let context = dummy_context().await; let xml = b"\n\n\n2019-03-06T21:09:57Z9.423110,53.790302\n\n \n\t2018-12-13T22:11:12Z\t 19.423110 \t , \n 63.790302\n \n\n"; diff --git a/src/log.rs b/src/log.rs index dbd298bcd..38e3c2f9e 100644 --- a/src/log.rs +++ b/src/log.rs @@ -7,9 +7,7 @@ macro_rules! info { }; ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ let formatted = format!($msg, $($args),*); - let thread = ::std::thread::current(); - let full = format!("{thid:?} {file}:{line}: {msg}", - thid = thread.id(), + let full = format!("{file}:{line}: {msg}", file = file!(), line = line!(), msg = &formatted); @@ -24,9 +22,7 @@ macro_rules! warn { }; ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ let formatted = format!($msg, $($args),*); - let thread = ::std::thread::current(); - let full = format!("{thid:?} {file}:{line}: {msg}", - thid = thread.id(), + let full = format!("{file}:{line}: {msg}", file = file!(), line = line!(), msg = &formatted); @@ -48,6 +44,6 @@ macro_rules! error { #[macro_export] macro_rules! emit_event { ($ctx:expr, $event:expr) => { - $ctx.call_cb($event); + $ctx.emit_event($event); }; } diff --git a/src/login_param.rs b/src/login_param.rs index be55feacd..7309a90ad 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -50,59 +50,69 @@ impl LoginParam { } /// Read the login parameters from the database. - pub fn from_database(context: &Context, prefix: impl AsRef) -> Self { + pub async fn from_database(context: &Context, prefix: impl AsRef) -> Self { let prefix = prefix.as_ref(); let sql = &context.sql; let key = format!("{}addr", prefix); let addr = sql .get_raw_config(context, key) + .await .unwrap_or_default() .trim() .to_string(); let key = format!("{}mail_server", prefix); - let mail_server = sql.get_raw_config(context, key).unwrap_or_default(); + let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default(); let key = format!("{}mail_port", prefix); - let mail_port = sql.get_raw_config_int(context, key).unwrap_or_default(); + let mail_port = sql + .get_raw_config_int(context, key) + .await + .unwrap_or_default(); let key = format!("{}mail_user", prefix); - let mail_user = sql.get_raw_config(context, key).unwrap_or_default(); + let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default(); let key = format!("{}mail_pw", prefix); - let mail_pw = sql.get_raw_config(context, key).unwrap_or_default(); + let mail_pw = sql.get_raw_config(context, key).await.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) { + if let Some(certificate_checks) = sql.get_raw_config_int(context, 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).unwrap_or_default(); + let send_server = sql.get_raw_config(context, key).await.unwrap_or_default(); let key = format!("{}send_port", prefix); - let send_port = sql.get_raw_config_int(context, key).unwrap_or_default(); + let send_port = sql + .get_raw_config_int(context, key) + .await + .unwrap_or_default(); let key = format!("{}send_user", prefix); - let send_user = sql.get_raw_config(context, key).unwrap_or_default(); + let send_user = sql.get_raw_config(context, key).await.unwrap_or_default(); let key = format!("{}send_pw", prefix); - let send_pw = sql.get_raw_config(context, key).unwrap_or_default(); + let send_pw = sql.get_raw_config(context, key).await.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) { + if let Some(certificate_checks) = sql.get_raw_config_int(context, 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).unwrap_or_default(); + let server_flags = sql + .get_raw_config_int(context, key) + .await + .unwrap_or_default(); LoginParam { addr, @@ -125,7 +135,7 @@ impl LoginParam { } /// Save this loginparam to the database. - pub fn save_to_database( + pub async fn save_to_database( &self, context: &Context, prefix: impl AsRef, @@ -134,40 +144,49 @@ impl LoginParam { let sql = &context.sql; let key = format!("{}addr", prefix); - sql.set_raw_config(context, key, Some(&self.addr))?; + sql.set_raw_config(context, key, Some(&self.addr)).await?; let key = format!("{}mail_server", prefix); - sql.set_raw_config(context, key, Some(&self.mail_server))?; + sql.set_raw_config(context, key, Some(&self.mail_server)) + .await?; let key = format!("{}mail_port", prefix); - sql.set_raw_config_int(context, key, self.mail_port)?; + sql.set_raw_config_int(context, key, self.mail_port).await?; let key = format!("{}mail_user", prefix); - sql.set_raw_config(context, key, Some(&self.mail_user))?; + sql.set_raw_config(context, key, Some(&self.mail_user)) + .await?; let key = format!("{}mail_pw", prefix); - sql.set_raw_config(context, key, Some(&self.mail_pw))?; + sql.set_raw_config(context, key, Some(&self.mail_pw)) + .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(context, key, self.imap_certificate_checks as i32) + .await?; let key = format!("{}send_server", prefix); - sql.set_raw_config(context, key, Some(&self.send_server))?; + sql.set_raw_config(context, key, Some(&self.send_server)) + .await?; let key = format!("{}send_port", prefix); - sql.set_raw_config_int(context, key, self.send_port)?; + sql.set_raw_config_int(context, key, self.send_port).await?; let key = format!("{}send_user", prefix); - sql.set_raw_config(context, key, Some(&self.send_user))?; + sql.set_raw_config(context, key, Some(&self.send_user)) + .await?; let key = format!("{}send_pw", prefix); - sql.set_raw_config(context, key, Some(&self.send_pw))?; + sql.set_raw_config(context, key, Some(&self.send_pw)) + .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(context, key, self.smtp_certificate_checks as i32) + .await?; let key = format!("{}server_flags", prefix); - sql.set_raw_config_int(context, key, self.server_flags)?; + sql.set_raw_config_int(context, key, self.server_flags) + .await?; Ok(()) } diff --git a/src/message.rs b/src/message.rs index bb067eb5e..1c77b4017 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,7 +1,6 @@ //! # Messages and their identifiers -use std::path::{Path, PathBuf}; - +use async_std::path::{Path, PathBuf}; use deltachat_derive::{FromSql, ToSql}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; @@ -13,12 +12,11 @@ use crate::context::*; use crate::dc_tools::*; use crate::error::{ensure, Error}; use crate::events::Event; -use crate::job::*; +use crate::job::{self, Action}; use crate::lot::{Lot, LotState, Meaning}; use crate::mimeparser::SystemMessage; use crate::param::*; use crate::pgp::*; -use crate::sql; use crate::stock::StockMessage; lazy_static! { @@ -88,32 +86,31 @@ impl MsgId { /// /// It means the message is deleted locally, but not on the server /// yet. - pub fn trash(self, context: &Context) -> crate::sql::Result<()> { + pub async fn trash(self, context: &Context) -> crate::sql::Result<()> { let chat_id = ChatId::new(DC_CHAT_ID_TRASH); - sql::execute( - context, - &context.sql, - "UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?", - params![chat_id, self], - ) + context + .sql + .execute( + "UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?", + paramsv![chat_id, self], + ) + .await?; + + Ok(()) } /// Deletes a message and corresponding MDNs from the database. - pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> { + pub async fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> { // We don't use transactions yet, so remove MDNs first to make // sure they are not left while the message is deleted. - sql::execute( - context, - &context.sql, - "DELETE FROM msgs_mdns WHERE msg_id=?;", - params![self], - )?; - sql::execute( - context, - &context.sql, - "DELETE FROM msgs WHERE id=?;", - params![self], - )?; + context + .sql + .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self]) + .await?; + context + .sql + .execute("DELETE FROM msgs WHERE id=?;", paramsv![self]) + .await?; Ok(()) } @@ -122,15 +119,17 @@ impl MsgId { /// It is used to avoid trying to remove the message from the /// server multiple times when there are multiple message records /// pointing to the same server UID. - pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> { - sql::execute( - context, - &context.sql, - "UPDATE msgs \ + pub(crate) async fn unlink(self, context: &Context) -> crate::sql::Result<()> { + context + .sql + .execute( + "UPDATE msgs \ SET server_folder='', server_uid=0 \ WHERE id=?", - params![self], - ) + paramsv![self], + ) + .await?; + Ok(()) } /// Bad evil escape hatch. @@ -267,12 +266,12 @@ impl Message { msg } - pub fn load_from_db(context: &Context, id: MsgId) -> Result { + pub async fn load_from_db(context: &Context, id: MsgId) -> Result { ensure!( !id.is_special(), "Can not load special message IDs from DB." ); - context + let msg = context .sql .query_row( concat!( @@ -300,7 +299,7 @@ impl Message { " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", " WHERE m.id=?;" ), - params![id], + paramsv![id], |row| { let mut msg = Message::default(); // msg.id = row.get::<_, AnyMsgId>("id")?; @@ -350,7 +349,9 @@ impl Message { Ok(msg) }, ) - .map_err(Into::into) + .await?; + + Ok(msg) } pub fn get_filemime(&self) -> Option { @@ -371,7 +372,7 @@ impl Message { self.param.get_path(Param::File, context).unwrap_or(None) } - pub fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<(), Error> { + pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<(), Error> { if chat::msgtype_has_file(self.viewtype) { let file_param = self.param.get_path(Param::File, context)?; if let Some(path_and_filename) = file_param { @@ -381,7 +382,7 @@ impl Message { self.param.set_int(Param::Width, 0); self.param.set_int(Param::Height, 0); - if let Ok(buf) = dc_read_file(context, path_and_filename) { + if let Ok(buf) = dc_read_file(context, path_and_filename).await { if let Ok((width, height)) = dc_get_filemeta(&buf) { self.param.set_int(Param::Width, width as i32); self.param.set_int(Param::Height, height as i32); @@ -389,7 +390,7 @@ impl Message { } if !self.id.is_unset() { - self.save_param_to_disk(context); + self.save_param_to_disk(context).await; } } } @@ -482,12 +483,12 @@ impl Message { .map(|name| name.to_string_lossy().to_string()) } - pub fn get_filebytes(&self, context: &Context) -> u64 { - self.param - .get_path(Param::File, context) - .unwrap_or(None) - .map(|path| dc_get_filebytes(context, &path)) - .unwrap_or_default() + pub async fn get_filebytes(&self, context: &Context) -> u64 { + match self.param.get_path(Param::File, context) { + Ok(Some(path)) => dc_get_filebytes(context, &path).await, + Ok(None) => 0, + Err(_) => 0, + } } pub fn get_width(&self) -> i32 { @@ -506,13 +507,13 @@ impl Message { self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0 } - pub fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot { + pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot { let mut ret = Lot::new(); let chat_loaded: Chat; let chat = if let Some(chat) = chat { chat - } else if let Ok(chat) = Chat::load_from_db(context, self.chat_id) { + } else if let Ok(chat) = Chat::load_from_db(context, self.chat_id).await { chat_loaded = chat; &chat_loaded } else { @@ -522,17 +523,17 @@ impl Message { let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) { - Contact::get_by_id(context, self.from_id).ok() + Contact::get_by_id(context, self.from_id).await.ok() } else { None }; - ret.fill(self, chat, contact.as_ref(), context); + ret.fill(self, chat, contact.as_ref(), context).await; ret } - pub fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String { + pub async fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String { get_summarytext_by_raw( self.viewtype, self.text.as_ref(), @@ -540,6 +541,7 @@ impl Message { approx_characters, context, ) + .await } pub fn has_deviating_timestamp(&self) -> bool { @@ -589,13 +591,13 @@ impl Message { self.param.get_cmd() == SystemMessage::AutocryptSetupMessage } - pub fn get_setupcodebegin(&self, context: &Context) -> Option { + pub async fn get_setupcodebegin(&self, context: &Context) -> Option { if !self.is_setupmessage() { return None; } if let Some(filename) = self.get_file(context) { - if let Ok(ref buf) = dc_read_file(context, filename) { + if let Ok(ref buf) = dc_read_file(context, filename).await { if let Ok((typ, headers, _)) = split_armored_data(buf) { if typ == pgp::armor::BlockType::Message { return headers.get(crate::pgp::HEADER_SETUPCODE).cloned(); @@ -627,7 +629,7 @@ impl Message { self.param.set_int(Param::Duration, duration); } - pub fn latefiling_mediasize( + pub async fn latefiling_mediasize( &mut self, context: &Context, width: i32, @@ -641,17 +643,18 @@ impl Message { if duration > 0 { self.param.set_int(Param::Duration, duration); } - self.save_param_to_disk(context); + self.save_param_to_disk(context).await; } - pub fn save_param_to_disk(&mut self, context: &Context) -> bool { - sql::execute( - context, - &context.sql, - "UPDATE msgs SET param=? WHERE id=?;", - params![self.param.to_string(), self.id], - ) - .is_ok() + pub async fn save_param_to_disk(&mut self, context: &Context) -> bool { + context + .sql + .execute( + "UPDATE msgs SET param=? WHERE id=?;", + paramsv![self.param.to_string(), self.id], + ) + .await + .is_ok() } } @@ -771,7 +774,7 @@ impl MessageState { impl Lot { /* library-internal */ /* in practice, the user additionally cuts the string himself pixel-accurate */ - pub fn fill( + pub async fn fill( &mut self, msg: &mut Message, chat: &Chat, @@ -779,14 +782,26 @@ impl Lot { context: &Context, ) { if msg.state == MessageState::OutDraft { - self.text1 = Some(context.stock_str(StockMessage::Draft).to_owned().into()); + self.text1 = Some( + context + .stock_str(StockMessage::Draft) + .await + .to_owned() + .into(), + ); self.text1_meaning = Meaning::Text1Draft; } else if msg.from_id == DC_CONTACT_ID_SELF { if msg.is_info() || chat.is_self_talk() { self.text1 = None; self.text1_meaning = Meaning::None; } else { - self.text1 = Some(context.stock_str(StockMessage::SelfMsg).to_owned().into()); + self.text1 = Some( + context + .stock_str(StockMessage::SelfMsg) + .await + .to_owned() + .into(), + ); self.text1_meaning = Meaning::Text1Self; } } else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { @@ -809,34 +824,40 @@ impl Lot { } } - self.text2 = Some(get_summarytext_by_raw( - msg.viewtype, - msg.text.as_ref(), - &msg.param, - SUMMARY_CHARACTERS, - context, - )); + self.text2 = Some( + get_summarytext_by_raw( + msg.viewtype, + msg.text.as_ref(), + &msg.param, + SUMMARY_CHARACTERS, + context, + ) + .await, + ); self.timestamp = msg.get_timestamp(); self.state = msg.state.into(); } } -pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { +pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String { let mut ret = String::new(); - let msg = Message::load_from_db(context, msg_id); + let msg = Message::load_from_db(context, msg_id).await; if msg.is_err() { return ret; } let msg = msg.unwrap_or_default(); - let rawtxt: Option = context.sql.query_get_value( - context, - "SELECT txt_raw FROM msgs WHERE id=?;", - params![msg_id], - ); + let rawtxt: Option = context + .sql + .query_get_value( + context, + "SELECT txt_raw FROM msgs WHERE id=?;", + paramsv![msg_id], + ) + .await; if rawtxt.is_none() { ret += &format!("Cannot load message {}.", msg_id); @@ -849,6 +870,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { ret += &format!("Sent: {}", fts); let name = Contact::load_from_db(context, msg.from_id) + .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default(); @@ -870,21 +892,26 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { return ret; } - if let Ok(rows) = context.sql.query_map( - "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;", - params![msg_id], - |row| { - let contact_id: i32 = row.get(0)?; - let ts: i64 = row.get(1)?; - Ok((contact_id, ts)) - }, - |rows| rows.collect::, _>>().map_err(Into::into), - ) { + if let Ok(rows) = context + .sql + .query_map( + "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;", + paramsv![msg_id], + |row| { + let contact_id: i32 = row.get(0)?; + let ts: i64 = row.get(1)?; + Ok((contact_id, ts)) + }, + |rows| rows.collect::, _>>().map_err(Into::into), + ) + .await + { for (contact_id, ts) in rows { let fts = dc_timestamp_to_str(ts); ret += &format!("Read: {}", fts); let name = Contact::load_from_db(context, contact_id as u32) + .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default(); @@ -915,7 +942,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { } if let Some(path) = msg.get_file(context) { - let bytes = dc_get_filebytes(context, &path); + let bytes = dc_get_filebytes(context, &path).await; ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes); } @@ -971,70 +998,78 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { Some(info) } -pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option { - context.sql.query_get_value( - context, - "SELECT mime_headers FROM msgs WHERE id=?;", - params![msg_id], - ) +pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option { + context + .sql + .query_get_value( + context, + "SELECT mime_headers FROM msgs WHERE id=?;", + paramsv![msg_id], + ) + .await } -pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) { - for msg_id in msg_ids { - if let Ok(msg) = Message::load_from_db(context, *msg_id) { +pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) { + for msg_id in msg_ids.iter() { + if let Ok(msg) = Message::load_from_db(context, *msg_id).await { if msg.location_id > 0 { - delete_poi_location(context, msg.location_id); + delete_poi_location(context, msg.location_id).await; } } - if let Err(err) = msg_id.trash(context) { + if let Err(err) = msg_id.trash(context).await { error!(context, "Unable to trash message {}: {}", msg_id, err); } - job_add( + job::add( context, - Action::DeleteMsgOnImap, - msg_id.to_u32() as i32, - Params::new(), - 0, - ); + job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0), + ) + .await; } if !msg_ids.is_empty() { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); - job_kill_action(context, Action::Housekeeping); - job_add(context, Action::Housekeeping, 0, Params::new(), 10); - }; + job::kill_action(context, Action::Housekeeping).await; + job::add( + context, + job::Job::new(Action::Housekeeping, 0, Params::new(), 10), + ) + .await; + } } -fn delete_poi_location(context: &Context, location_id: u32) -> bool { - sql::execute( - context, - &context.sql, - "DELETE FROM locations WHERE independent = 1 AND id=?;", - params![location_id as i32], - ) - .is_ok() +async fn delete_poi_location(context: &Context, location_id: u32) -> bool { + context + .sql + .execute( + "DELETE FROM locations WHERE independent = 1 AND id=?;", + paramsv![location_id as i32], + ) + .await + .is_ok() } -pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool { +pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> bool { if msg_ids.is_empty() { return false; } - let msgs = context.sql.prepare( - concat!( - "SELECT", - " m.state AS state,", - " c.blocked AS blocked", - " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", - " WHERE m.id=? AND m.chat_id>9" - ), - |mut stmt, _| { - let mut res = Vec::with_capacity(msg_ids.len()); - for id in msg_ids.iter() { - let query_res = stmt.query_row(params![*id], |row| { + let msgs = context + .sql + .with_conn(move |conn| { + let mut stmt = conn.prepare_cached(concat!( + "SELECT", + " m.state AS state,", + " c.blocked AS blocked", + " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", + " WHERE m.id=? AND m.chat_id>9" + ))?; + + let mut msgs = Vec::with_capacity(msg_ids.len()); + for id in msg_ids.into_iter() { + let query_res = stmt.query_row(paramsv![id], |row| { Ok(( row.get::<_, MessageState>("state")?, row.get::<_, Option>("blocked")? @@ -1044,44 +1079,38 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool { if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res { continue; } - let (state, blocked) = query_res?; - res.push((id, state, blocked)); + let (state, blocked) = query_res.map_err(Into::::into)?; + msgs.push((id, state, blocked)); } - Ok(res) - }, - ); + Ok(msgs) + }) + .await + .unwrap_or_default(); - if msgs.is_err() { - warn!(context, "markseen_msgs failed: {:?}", msgs); - return false; - } let mut send_event = false; - let msgs = msgs.unwrap_or_default(); for (id, curr_state, curr_blocked) in msgs.into_iter() { if curr_blocked == Blocked::Not { if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed { - update_msg_state(context, *id, MessageState::InSeen); + update_msg_state(context, id, MessageState::InSeen).await; info!(context, "Seen message {}.", id); - job_add( + job::add( context, - Action::MarkseenMsgOnImap, - id.to_u32() as i32, - Params::new(), - 0, - ); + job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0), + ) + .await; send_event = true; } } else if curr_state == MessageState::InFresh { - update_msg_state(context, *id, MessageState::InNoticed); + update_msg_state(context, id, MessageState::InNoticed).await; send_event = true; } } if send_event { - context.call_cb(Event::MsgsChanged { + context.emit_event(Event::MsgsChanged { chat_id: ChatId::new(0), msg_id: MsgId::new(0), }); @@ -1090,33 +1119,36 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool { true } -pub fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageState) -> bool { - sql::execute( - context, - &context.sql, - "UPDATE msgs SET state=? WHERE id=?;", - params![state, msg_id], - ) - .is_ok() +pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageState) -> bool { + context + .sql + .execute( + "UPDATE msgs SET state=? WHERE id=?;", + paramsv![state, msg_id], + ) + .await + .is_ok() } -pub fn star_msgs(context: &Context, msg_ids: &[MsgId], star: bool) -> bool { +pub async fn star_msgs(context: &Context, msg_ids: Vec, star: bool) -> bool { if msg_ids.is_empty() { return false; } context .sql - .prepare("UPDATE msgs SET starred=? WHERE id=?;", |mut stmt, _| { - for msg_id in msg_ids.iter() { - stmt.execute(params![star as i32, *msg_id])?; + .with_conn(move |conn| { + let mut stmt = conn.prepare("UPDATE msgs SET starred=? WHERE id=?;")?; + for msg_id in msg_ids.into_iter() { + stmt.execute(paramsv![star as i32, msg_id])?; } Ok(()) }) + .await .is_ok() } /// Returns a summary test. -pub fn get_summarytext_by_raw( +pub async fn get_summarytext_by_raw( viewtype: Viewtype, text: Option>, param: &Params, @@ -1125,16 +1157,20 @@ pub fn get_summarytext_by_raw( ) -> String { let mut append_text = true; let prefix = match viewtype { - Viewtype::Image => context.stock_str(StockMessage::Image).into_owned(), - Viewtype::Gif => context.stock_str(StockMessage::Gif).into_owned(), - Viewtype::Sticker => context.stock_str(StockMessage::Sticker).into_owned(), - Viewtype::Video => context.stock_str(StockMessage::Video).into_owned(), - Viewtype::Voice => context.stock_str(StockMessage::VoiceMessage).into_owned(), + Viewtype::Image => context.stock_str(StockMessage::Image).await.into_owned(), + Viewtype::Gif => context.stock_str(StockMessage::Gif).await.into_owned(), + Viewtype::Sticker => context.stock_str(StockMessage::Sticker).await.into_owned(), + Viewtype::Video => context.stock_str(StockMessage::Video).await.into_owned(), + Viewtype::Voice => context + .stock_str(StockMessage::VoiceMessage) + .await + .into_owned(), Viewtype::Audio | Viewtype::File => { if param.get_cmd() == SystemMessage::AutocryptSetupMessage { append_text = false; context .stock_str(StockMessage::AcSetupMsgSubject) + .await .to_string() } else { let file_name: String = param @@ -1145,11 +1181,13 @@ pub fn get_summarytext_by_raw( .map(|fname| fname.to_string_lossy().into_owned()) }) .unwrap_or_else(|| String::from("ErrFileName")); - let label = context.stock_str(if viewtype == Viewtype::Audio { - StockMessage::Audio - } else { - StockMessage::File - }); + let label = context + .stock_str(if viewtype == Viewtype::Audio { + StockMessage::Audio + } else { + StockMessage::File + }) + .await; format!("{} – {}", label, file_name) } } @@ -1158,7 +1196,7 @@ pub fn get_summarytext_by_raw( "".to_string() } else { append_text = false; - context.stock_str(StockMessage::Location).to_string() + context.stock_str(StockMessage::Location).await.to_string() } } }; @@ -1190,16 +1228,19 @@ pub fn get_summarytext_by_raw( // Context functions to work with messages -pub fn exists(context: &Context, msg_id: MsgId) -> bool { +pub async fn exists(context: &Context, msg_id: MsgId) -> bool { if msg_id.is_special() { return false; } - let chat_id: Option = context.sql.query_get_value( - context, - "SELECT chat_id FROM msgs WHERE id=?;", - params![msg_id], - ); + let chat_id: Option = context + .sql + .query_get_value( + context, + "SELECT chat_id FROM msgs WHERE id=?;", + paramsv![msg_id], + ) + .await; if let Some(chat_id) = chat_id { !chat_id.is_trash() @@ -1208,8 +1249,8 @@ pub fn exists(context: &Context, msg_id: MsgId) -> bool { } } -pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option>) { - if let Ok(mut msg) = Message::load_from_db(context, msg_id) { +pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option>) { + if let Ok(mut msg) = Message::load_from_db(context, msg_id).await { if msg.state.can_fail() { msg.state = MessageState::OutFailed; } @@ -1218,15 +1259,16 @@ pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option("msg_id")?, - row.get::<_, ChatId>("chat_id")?, - row.get::<_, Chattype>("type")?, - row.get::<_, MessageState>("state")?, - )) - }, - ); + let res = context + .sql + .query_row( + concat!( + "SELECT", + " m.id AS msg_id,", + " c.id AS chat_id,", + " c.type AS type,", + " m.state AS state", + " FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id", + " WHERE rfc724_mid=? AND from_id=1", + " ORDER BY m.id;" + ), + paramsv![rfc724_mid], + |row| { + Ok(( + row.get::<_, MsgId>("msg_id")?, + row.get::<_, ChatId>("chat_id")?, + row.get::<_, Chattype>("type")?, + row.get::<_, MessageState>("state")?, + )) + }, + ) + .await; if let Err(ref err) = res { info!(context, "Failed to select MDN {:?}", err); } @@ -1281,30 +1326,34 @@ pub fn mdn_from_ext( .sql .exists( "SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;", - params![msg_id, from_id as i32,], + paramsv![msg_id, from_id as i32,], ) + .await .unwrap_or_default(); if !mdn_already_in_table { context.sql.execute( "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);", - params![msg_id, from_id as i32, timestamp_sent], - ).unwrap_or_default(); // TODO: better error handling + paramsv![msg_id, from_id as i32, timestamp_sent], + ) + .await + .unwrap_or_default(); // TODO: better error handling } // Normal chat? that's quite easy. if chat_type == Chattype::Single { - update_msg_state(context, msg_id, MessageState::OutMdnRcvd); + update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await; read_by_all = true; } else { // send event about new state let ist_cnt = context .sql - .query_get_value::<_, isize>( + .query_get_value::( context, "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;", - params![msg_id], + paramsv![msg_id], ) + .await .unwrap_or_default() as usize; /* Groupsize: Min. MDNs @@ -1319,9 +1368,9 @@ pub fn mdn_from_ext( (S=Sender, R=Recipient) */ // for rounding, SELF is already included! - let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id) + 1) / 2; + let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id).await + 1) / 2; if ist_cnt >= soll_cnt { - update_msg_state(context, msg_id, MessageState::OutMdnRcvd); + update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await; read_by_all = true; } // else wait for more receipts } @@ -1336,14 +1385,18 @@ pub fn mdn_from_ext( } /// The number of messages assigned to real chat (!=deaddrop, !=trash) -pub fn get_real_msg_cnt(context: &Context) -> i32 { - match context.sql.query_row( - "SELECT COUNT(*) \ +pub async fn get_real_msg_cnt(context: &Context) -> i32 { + match context + .sql + .query_row( + "SELECT COUNT(*) \ FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;", - rusqlite::NO_PARAMS, - |row| row.get(0), - ) { + paramsv![], + |row| row.get(0), + ) + .await + { Ok(res) => res, Err(err) => { error!(context, "dc_get_real_msg_cnt() failed. {}", err); @@ -1352,14 +1405,18 @@ pub fn get_real_msg_cnt(context: &Context) -> i32 { } } -pub fn get_deaddrop_msg_cnt(context: &Context) -> usize { - match context.sql.query_row( - "SELECT COUNT(*) \ +pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize { + match context + .sql + .query_row( + "SELECT COUNT(*) \ FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ WHERE c.blocked=2;", - rusqlite::NO_PARAMS, - |row| row.get::<_, isize>(0), - ) { + paramsv![], + |row| row.get::<_, isize>(0), + ) + .await + { Ok(res) => res as usize, Err(err) => { error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err); @@ -1368,43 +1425,50 @@ pub fn get_deaddrop_msg_cnt(context: &Context) -> usize { } } -pub fn estimate_deletion_cnt( +pub async fn estimate_deletion_cnt( context: &Context, from_server: bool, seconds: i64, ) -> Result { let self_chat_id = chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF) + .await .unwrap_or_default() .0; let threshold_timestamp = time() - seconds; let cnt: isize = if from_server { - context.sql.query_row( - "SELECT COUNT(*) + context + .sql + .query_row( + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND server_uid != 0;", - params![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], - |row| row.get(0), - )? + paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], + |row| row.get(0), + ) + .await? } else { - context.sql.query_row( - "SELECT COUNT(*) + context + .sql + .query_row( + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND chat_id != ? AND hidden = 0;", - params![ - DC_MSG_ID_LAST_SPECIAL, - threshold_timestamp, - self_chat_id, - ChatId::new(DC_CHAT_ID_TRASH) - ], - |row| row.get(0), - )? + paramsv![ + DC_MSG_ID_LAST_SPECIAL, + threshold_timestamp, + self_chat_id, + ChatId::new(DC_CHAT_ID_TRASH) + ], + |row| row.get(0), + ) + .await? }; Ok(cnt as usize) } @@ -1413,13 +1477,17 @@ pub fn estimate_deletion_cnt( /// Message-ID. /// /// Unlinked messages are excluded. -pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 { +pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 { // check the number of messages with the same rfc724_mid - match context.sql.query_row( - "SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0", - &[rfc724_mid], - |row| row.get(0), - ) { + match context + .sql + .query_row( + "SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0", + paramsv![rfc724_mid], + |row| row.get(0), + ) + .await + { Ok(res) => res, Err(err) => { error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err); @@ -1428,7 +1496,7 @@ pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 { } } -pub(crate) fn rfc724_mid_exists( +pub(crate) async fn rfc724_mid_exists( context: &Context, rfc724_mid: &str, ) -> Result, Error> { @@ -1437,11 +1505,11 @@ pub(crate) fn rfc724_mid_exists( return Ok(None); } - context + let res = context .sql .query_row_optional( "SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?", - &[rfc724_mid], + paramsv![rfc724_mid], |row| { let server_folder = row.get::<_, Option>(0)?.unwrap_or_default(); let server_uid = row.get(1)?; @@ -1450,20 +1518,26 @@ pub(crate) fn rfc724_mid_exists( Ok((server_folder, server_uid, msg_id)) }, ) - .map_err(Into::into) + .await?; + + Ok(res) } -pub fn update_server_uid( +pub async fn update_server_uid( context: &Context, rfc724_mid: &str, server_folder: impl AsRef, server_uid: u32, ) { - match context.sql.execute( - "UPDATE msgs SET server_folder=?, server_uid=? \ - WHERE rfc724_mid=?", - params![server_folder.as_ref(), server_uid, rfc724_mid], - ) { + match context + .sql + .execute( + "UPDATE msgs SET server_folder=?, server_uid=? \ + WHERE rfc724_mid=?", + paramsv![server_folder.as_ref(), server_uid, rfc724_mid], + ) + .await + { Ok(_) => {} Err(err) => { warn!(context, "msg: failed to update server_uid: {}", err); @@ -1472,9 +1546,13 @@ pub fn update_server_uid( } #[allow(dead_code)] -pub fn dc_empty_server(context: &Context, flags: u32) { - job_kill_action(context, Action::EmptyServer); - job_add(context, Action::EmptyServer, flags as i32, Params::new(), 0); +pub async fn dc_empty_server(context: &Context, flags: u32) { + job::kill_action(context, Action::EmptyServer).await; + job::add( + context, + job::Job::new(Action::EmptyServer, flags, Params::new(), 0), + ) + .await; } #[cfg(test)] @@ -1490,32 +1568,35 @@ mod tests { ); } - #[test] - pub fn test_prepare_message_and_send() { + #[async_std::test] + async fn test_prepare_message_and_send() { use crate::config::Config; - let d = test::dummy_context(); + let d = test::dummy_context().await; let ctx = &d.ctx; - let contact = - Contact::create(ctx, "", "dest@example.com").expect("failed to create contact"); + let contact = Contact::create(ctx, "", "dest@example.com") + .await + .expect("failed to create contact"); - let res = ctx.set_config(Config::ConfiguredAddr, Some("self@example.com")); + let res = ctx + .set_config(Config::ConfiguredAddr, Some("self@example.com")) + .await; assert!(res.is_ok()); - let chat = chat::create_by_contact_id(ctx, contact).unwrap(); + let chat = chat::create_by_contact_id(ctx, contact).await.unwrap(); let mut msg = Message::new(Viewtype::Text); - let msg_id = chat::prepare_msg(ctx, chat, &mut msg).unwrap(); + let msg_id = chat::prepare_msg(ctx, chat, &mut msg).await.unwrap(); - let _msg2 = Message::load_from_db(ctx, msg_id).unwrap(); + let _msg2 = Message::load_from_db(ctx, msg_id).await.unwrap(); assert_eq!(_msg2.get_filemime(), None); } - #[test] - pub fn test_get_summarytext_by_raw() { - let d = test::dummy_context(); + #[async_std::test] + async fn test_get_summarytext_by_raw() { + let d = test::dummy_context().await; let ctx = &d.ctx; let some_text = Some("bla bla".to_string()); @@ -1526,62 +1607,69 @@ mod tests { some_file.set(Param::File, "foo.bar"); assert_eq!( - get_summarytext_by_raw(Viewtype::Text, some_text.as_ref(), &Params::new(), 50, &ctx), + get_summarytext_by_raw(Viewtype::Text, some_text.as_ref(), &Params::new(), 50, &ctx) + .await, "bla bla" // for simple text, the type is not added to the summary ); assert_eq!( - get_summarytext_by_raw(Viewtype::Image, no_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Image, no_text.as_ref(), &some_file, 50, &ctx).await, "Image" // file names are not added for images ); assert_eq!( - get_summarytext_by_raw(Viewtype::Video, no_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Video, no_text.as_ref(), &some_file, 50, &ctx).await, "Video" // file names are not added for videos ); assert_eq!( - get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), &some_file, 50, &ctx,).await, "GIF" // file names are not added for GIFs ); assert_eq!( - get_summarytext_by_raw(Viewtype::Sticker, no_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Sticker, no_text.as_ref(), &some_file, 50, &ctx,) + .await, "Sticker" // file names are not added for stickers ); assert_eq!( - get_summarytext_by_raw(Viewtype::Voice, empty_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Voice, empty_text.as_ref(), &some_file, 50, &ctx,) + .await, "Voice message" // file names are not added for voice messages, empty text is skipped ); assert_eq!( - get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &mut some_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &mut some_file, 50, &ctx) + .await, "Voice message" // file names are not added for voice messages ); assert_eq!( - get_summarytext_by_raw(Viewtype::Voice, some_text.as_ref(), &some_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::Voice, some_text.as_ref(), &some_file, 50, &ctx).await, "Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH" ); assert_eq!( - get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &mut some_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &mut some_file, 50, &ctx) + .await, "Audio \u{2013} foo.bar" // file name is added for audio ); assert_eq!( - get_summarytext_by_raw(Viewtype::Audio, empty_text.as_ref(), &some_file, 50, &ctx,), + get_summarytext_by_raw(Viewtype::Audio, empty_text.as_ref(), &some_file, 50, &ctx,) + .await, "Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added ); assert_eq!( - get_summarytext_by_raw(Viewtype::Audio, some_text.as_ref(), &some_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::Audio, some_text.as_ref(), &some_file, 50, &ctx).await, "Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio ); assert_eq!( - get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &mut some_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &mut some_file, 50, &ctx) + .await, "File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files ); @@ -1589,7 +1677,7 @@ mod tests { asm_file.set(Param::File, "foo.bar"); asm_file.set_cmd(SystemMessage::AutocryptSetupMessage); assert_eq!( - get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &mut asm_file, 50, &ctx), + get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &mut asm_file, 50, &ctx).await, "Autocrypt Setup Message" // file name is not added for autocrypt setup messages ); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 078e608e5..e17cf668e 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -66,74 +66,89 @@ pub struct RenderedEmail { } impl<'a, 'b> MimeFactory<'a, 'b> { - pub fn from_msg( + pub async fn from_msg( context: &'a Context, msg: &'b Message, attach_selfavatar: bool, ) -> Result, Error> { - let chat = Chat::load_from_db(context, msg.chat_id)?; + let chat = Chat::load_from_db(context, msg.chat_id).await?; let from_addr = context .get_config(Config::ConfiguredAddr) + .await + .unwrap_or_default(); + let from_displayname = context + .get_config(Config::Displayname) + .await .unwrap_or_default(); - let from_displayname = context.get_config(Config::Displayname).unwrap_or_default(); let mut recipients = Vec::with_capacity(5); let mut req_mdn = false; if chat.is_self_talk() { recipients.push((from_displayname.to_string(), from_addr.to_string())); } else { - context.sql.query_map( - "SELECT c.authname, c.addr \ + context + .sql + .query_map( + "SELECT c.authname, c.addr \ FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ WHERE cc.chat_id=? AND cc.contact_id>9;", - params![msg.chat_id], - |row| { - let authname: String = row.get(0)?; - let addr: String = row.get(1)?; - Ok((authname, addr)) - }, - |rows| { - for row in rows { - let (authname, addr) = row?; - if !recipients_contain_addr(&recipients, &addr) { - recipients.push((authname, addr)); + 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(()) - }, - )?; + Ok(()) + }, + ) + .await?; let command = msg.param.get_cmd(); if command != SystemMessage::AutocryptSetupMessage && command != SystemMessage::SecurejoinMessage - && context.get_config_bool(Config::MdnsEnabled) + && context.get_config_bool(Config::MdnsEnabled).await { req_mdn = true; } } - let (in_reply_to, references) = context.sql.query_row( - "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", - params![msg.id], - |row| { - let in_reply_to: String = row.get(0)?; - let references: String = row.get(1)?; + let (in_reply_to, references) = context + .sql + .query_row( + "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", + paramsv![msg.id], + |row| { + let in_reply_to: String = row.get(0)?; + let references: String = row.get(1)?; - Ok(( - render_rfc724_mid_list(&in_reply_to), - render_rfc724_mid_list(&references), - )) - }, - )?; + Ok(( + render_rfc724_mid_list(&in_reply_to), + render_rfc724_mid_list(&references), + )) + }, + ) + .await?; + let default_str = context + .stock_str(StockMessage::StatusLine) + .await + .to_string(); let factory = MimeFactory { from_addr, from_displayname, selfstatus: context .get_config(Config::Selfstatus) - .unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()), + .await + .unwrap_or_else(|| default_str), recipients, timestamp: msg.timestamp_sort, loaded: Loaded::Message { chat }, @@ -148,29 +163,42 @@ impl<'a, 'b> MimeFactory<'a, 'b> { Ok(factory) } - pub fn from_mdn( + pub async fn from_mdn( context: &'a Context, msg: &'b Message, additional_msg_ids: Vec, - ) -> Result { + ) -> Result, Error> { ensure!(!msg.chat_id.is_special(), "Invalid chat id"); - let contact = Contact::load_from_db(context, msg.from_id)?; + let contact = Contact::load_from_db(context, msg.from_id).await?; + let from_addr = context + .get_config(Config::ConfiguredAddr) + .await + .unwrap_or_default(); + let from_displayname = context + .get_config(Config::Displayname) + .await + .unwrap_or_default(); + let default_str = context + .stock_str(StockMessage::StatusLine) + .await + .to_string(); + let selfstatus = context + .get_config(Config::Selfstatus) + .await + .unwrap_or_else(|| default_str); + let timestamp = dc_create_smeared_timestamp(context).await; - Ok(MimeFactory { + let res = MimeFactory::<'a, 'b> { context, - from_addr: context - .get_config(Config::ConfiguredAddr) - .unwrap_or_default(), - from_displayname: context.get_config(Config::Displayname).unwrap_or_default(), - selfstatus: context - .get_config(Config::Selfstatus) - .unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()), + from_addr, + from_displayname, + selfstatus, recipients: vec![( contact.get_authname().to_string(), contact.get_addr().to_string(), )], - timestamp: dc_create_smeared_timestamp(context), + timestamp, loaded: Loaded::MDN { additional_msg_ids }, msg, in_reply_to: String::default(), @@ -178,26 +206,31 @@ impl<'a, 'b> MimeFactory<'a, 'b> { req_mdn: false, last_added_location_id: 0, attach_selfavatar: false, - }) + }; + + Ok(res) } - fn peerstates_for_recipients(&self) -> Result, &str)>, Error> { + async fn peerstates_for_recipients(&self) -> Result>, &str)>, Error> { let self_addr = self .context .get_config(Config::ConfiguredAddr) + .await .ok_or_else(|| format_err!("Not configured"))?; - Ok(self + let mut res = Vec::new(); + for (_, addr) in self .recipients .iter() .filter(|(_, addr)| addr != &self_addr) - .map(|(_, addr)| { - ( - Peerstate::from_addr(self.context, &self.context.sql, addr), - addr.as_str(), - ) - }) - .collect()) + { + res.push(( + Peerstate::from_addr(self.context, addr).await, + addr.as_str(), + )); + } + + Ok(res) } fn is_e2ee_guaranteed(&self) -> bool { @@ -257,11 +290,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } } - fn should_do_gossip(&self) -> bool { + async fn should_do_gossip(&self) -> bool { match &self.loaded { Loaded::Message { chat } => { // beside key- and member-changes, force re-gossip every 48 hours - let gossiped_timestamp = chat.get_gossiped_timestamp(self.context); + let gossiped_timestamp = chat.get_gossiped_timestamp(self.context).await; if time() > gossiped_timestamp + (2 * 24 * 60 * 60) { return true; } @@ -302,12 +335,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } } - fn subject_str(&self) -> String { + async fn subject_str(&self) -> String { match self.loaded { Loaded::Message { ref chat } => { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { self.context .stock_str(StockMessage::AcSetupMsgSubject) + .await .into_owned() } else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { let re = if self.in_reply_to.is_empty() { @@ -323,12 +357,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> { &self.msg.param, 32, self.context, - ); + ) + .await; let raw_subject = raw.lines().next().unwrap_or_default(); format!("Chat: {}", raw_subject) } } - Loaded::MDN { .. } => self.context.stock_str(StockMessage::ReadRcpt).into_owned(), + Loaded::MDN { .. } => self + .context + .stock_str(StockMessage::ReadRcpt) + .await + .into_owned(), } } @@ -339,7 +378,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .collect() } - pub fn render(mut self) -> Result { + pub async fn render(mut self) -> Result { // Headers that are encrypted // - Chat-*, except Chat-Version // - Secure-Join* @@ -422,17 +461,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> { let min_verified = self.min_verified(); let grpimage = self.grpimage(); let force_plaintext = self.should_force_plaintext(); - let subject_str = self.subject_str(); + let subject_str = self.subject_str().await; let e2ee_guaranteed = self.is_e2ee_guaranteed(); - let mut encrypt_helper = EncryptHelper::new(self.context)?; + let encrypt_helper = EncryptHelper::new(self.context).await?; let subject = encode_words(&subject_str); let mut message = match self.loaded { Loaded::Message { .. } => { - self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)? + self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage) + .await? } - Loaded::MDN { .. } => self.render_mdn()?, + Loaded::MDN { .. } => self.render_mdn().await?, }; if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 { @@ -443,7 +483,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { protected_headers.push(Header::new("Subject".into(), subject)); - let peerstates = self.peerstates_for_recipients()?; + let peerstates = self.peerstates_for_recipients().await?; let should_encrypt = encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?; let is_encrypted = should_encrypt && force_plaintext == 0; @@ -471,7 +511,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { let outer_message = if is_encrypted { // Add gossip headers in chats with multiple recipients - if peerstates.len() > 1 && self.should_do_gossip() { + if peerstates.len() > 1 && self.should_do_gossip().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) { @@ -519,8 +559,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> { println!("{}", raw_message); } - let encrypted = - encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?; + let encrypted = encrypt_helper + .encrypt(self.context, min_verified, message, peerstates) + .await?; outer_message = outer_message .child( @@ -592,9 +633,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> { Some(part) } - fn get_location_kml_part(&mut self) -> Result { + async fn get_location_kml_part(&mut self) -> Result { let (kml_content, last_added_location_id) = - location::get_kml(self.context, self.msg.chat_id)?; + location::get_kml(self.context, self.msg.chat_id).await?; let part = PartBuilder::new() .content_type( &"application/vnd.google-earth.kml+xml" @@ -614,7 +655,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } #[allow(clippy::cognitive_complexity)] - fn render_message( + async fn render_message( &mut self, protected_headers: &mut Vec
, unprotected_headers: &mut Vec
, @@ -709,6 +750,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { placeholdertext = Some( self.context .stock_str(StockMessage::AcSetupMsgBody) + .await .to_string(), ); } @@ -755,7 +797,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { meta.viewtype = Viewtype::Image; meta.param.set(Param::File, grpimage); - let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?; + let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?; meta_part = Some(mail); protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent)); } @@ -826,13 +868,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> { // add attachment part if chat::msgtype_has_file(self.msg.viewtype) { - if !is_file_size_okay(context, &self.msg) { + if !is_file_size_okay(context, &self.msg).await { bail!( "Message exceeds the recommended {} MB.", RECOMMENDED_FILE_SIZE / 1_000_000, ); } else { - let (file_part, _) = build_body_file(context, &self.msg, "")?; + let (file_part, _) = build_body_file(context, &self.msg, "").await?; parts.push(file_part); } } @@ -845,8 +887,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> { parts.push(msg_kml_part); } - if location::is_sending_locations_to_chat(context, self.msg.chat_id) { - match self.get_location_kml_part() { + if location::is_sending_locations_to_chat(context, self.msg.chat_id).await { + match self.get_location_kml_part().await { Ok(part) => parts.push(part), Err(err) => { warn!(context, "mimefactory: could not send location: {}", err); @@ -855,7 +897,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } if self.attach_selfavatar { - match context.get_config(Config::Selfavatar) { + match context.get_config(Config::Selfavatar).await { Some(path) => match build_selfavatar_file(context, &path) { Ok((part, filename)) => { parts.push(part); @@ -882,7 +924,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } /// Render an MDN - fn render_mdn(&mut self) -> Result { + async fn render_mdn(&mut self) -> Result { // RFC 6522, this also requires the `report-type` parameter which is equal // to the MIME subtype of the second body part of the multipart/report // @@ -917,13 +959,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> { { self.context .stock_str(StockMessage::EncryptedMsg) + .await .into_owned() } else { - self.msg.get_summarytext(self.context, 32) + self.msg.get_summarytext(self.context, 32).await }; let p2 = self .context - .stock_string_repl_str(StockMessage::ReadRcptMailBody, p1); + .stock_string_repl_str(StockMessage::ReadRcptMailBody, p1) + .await; let message_text = format!("{}\r\n", p2); message = message.child( PartBuilder::new() @@ -980,14 +1024,15 @@ fn wrapped_base64_encode(buf: &[u8]) -> String { .join("\r\n") } -fn build_body_file( +async fn build_body_file( context: &Context, msg: &Message, base_name: &str, ) -> Result<(PartBuilder, String), Error> { let blob = msg .param - .get_blob(Param::File, context, true)? + .get_blob(Param::File, context, true) + .await? .ok_or_else(|| format_err!("msg has no filename"))?; let suffix = blob.suffix().unwrap_or("dat"); @@ -1083,10 +1128,10 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool .any(|(_, cur)| cur.to_lowercase() == addr_lc) } -fn is_file_size_okay(context: &Context, msg: &Message) -> bool { +async fn is_file_size_okay(context: &Context, msg: &Message) -> bool { match msg.param.get_path(Param::File, context).unwrap_or(None) { Some(path) => { - let bytes = dc_get_filebytes(context, &path); + let bytes = dc_get_filebytes(context, &path).await; bytes <= UPPER_LIMIT_FILE_SIZE } None => false, diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 1c4348eea..aa87b9143 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,4 +1,6 @@ use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; use deltachat_derive::{FromSql, ToSql}; use lettre_email::mime::{self, Mime}; @@ -82,7 +84,7 @@ impl Default for SystemMessage { const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; impl MimeMessage { - pub fn from_bytes(context: &Context, body: &[u8]) -> Result { + pub async fn from_bytes(context: &Context, body: &[u8]) -> Result { let mail = mailparse::parse_mail(body)?; let message_time = mail @@ -113,7 +115,7 @@ impl MimeMessage { let mail_raw; let mut gossipped_addr = Default::default(); - let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time) { + let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time).await { Ok((raw, signatures)) => { if let Some(raw) = raw { // Valid autocrypt message, encrypted @@ -128,7 +130,8 @@ impl MimeMessage { // "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); gossipped_addr = - update_gossip_peerstates(context, message_time, &mail, gossip_headers)?; + update_gossip_peerstates(context, message_time, &mail, gossip_headers) + .await?; // let known protected headers from the decrypted // part override the unencrypted top-level @@ -179,7 +182,7 @@ impl MimeMessage { user_avatar: None, group_avatar: None, }; - parser.parse_mime_recursive(context, &mail)?; + parser.parse_mime_recursive(context, &mail).await?; parser.parse_headers(context)?; Ok(parser) @@ -410,63 +413,69 @@ impl MimeMessage { self.header.get(headerdef.get_headername()) } - fn parse_mime_recursive( - &mut self, - context: &Context, - mail: &mailparse::ParsedMail<'_>, - ) -> Result { - if mail.ctype.params.get("protected-headers").is_some() { - if mail.ctype.mimetype == "text/rfc822-headers" { - warn!( - context, - "Protected headers found in text/rfc822-headers attachment: Will be ignored.", - ); - return Ok(false); - } + fn parse_mime_recursive<'a>( + &'a mut self, + context: &'a Context, + mail: &'a mailparse::ParsedMail<'a>, + ) -> Pin> + 'a + Send>> { + use futures::future::FutureExt; - warn!(context, "Ignoring nested protected headers"); - } - - enum MimeS { - Multiple, - Single, - Message, - } - - let mimetype = mail.ctype.mimetype.to_lowercase(); - - let m = if mimetype.starts_with("multipart") { - if mail.ctype.params.get("boundary").is_some() { - MimeS::Multiple - } else { - MimeS::Single - } - } else if mimetype.starts_with("message") { - if mimetype == "message/rfc822" { - MimeS::Message - } else { - MimeS::Single - } - } else { - MimeS::Single - }; - - match m { - MimeS::Multiple => self.handle_multiple(context, mail), - MimeS::Message => { - let raw = mail.get_body_raw()?; - if raw.is_empty() { + // Boxed future to deal with recursion + async move { + if mail.ctype.params.get("protected-headers").is_some() { + if mail.ctype.mimetype == "text/rfc822-headers" { + warn!( + context, + "Protected headers found in text/rfc822-headers attachment: Will be ignored.", + ); return Ok(false); } - let mail = mailparse::parse_mail(&raw).unwrap(); - self.parse_mime_recursive(context, &mail) + warn!(context, "Ignoring nested protected headers"); + } + + enum MimeS { + Multiple, + Single, + Message, + } + + let mimetype = mail.ctype.mimetype.to_lowercase(); + + let m = if mimetype.starts_with("multipart") { + if mail.ctype.params.get("boundary").is_some() { + MimeS::Multiple + } else { + MimeS::Single + } + } else if mimetype.starts_with("message") { + if mimetype == "message/rfc822" { + MimeS::Message + } else { + MimeS::Single + } + } else { + MimeS::Single + }; + + match m { + MimeS::Multiple => self.handle_multiple(context, mail).await, + MimeS::Message => { + let raw = mail.get_body_raw()?; + if raw.is_empty() { + return Ok(false); + } + let mail = mailparse::parse_mail(&raw).unwrap(); + + self.parse_mime_recursive(context, &mail).await + } + MimeS::Single => self.add_single_part_if_known(context, mail).await, } - MimeS::Single => self.add_single_part_if_known(context, mail), } + .boxed() } - fn handle_multiple( + async fn handle_multiple( &mut self, context: &Context, mail: &mailparse::ParsedMail<'_>, @@ -483,7 +492,7 @@ impl MimeMessage { if get_mime_type(cur_data)?.0 == "multipart/mixed" || get_mime_type(cur_data)?.0 == "multipart/related" { - any_part_added = self.parse_mime_recursive(context, cur_data)?; + any_part_added = self.parse_mime_recursive(context, cur_data).await?; break; } } @@ -491,7 +500,7 @@ impl MimeMessage { /* search for text/plain and add this */ for cur_data in &mail.subparts { if get_mime_type(cur_data)?.0.type_() == mime::TEXT { - any_part_added = self.parse_mime_recursive(context, cur_data)?; + any_part_added = self.parse_mime_recursive(context, cur_data).await?; break; } } @@ -499,7 +508,7 @@ impl MimeMessage { if !any_part_added { /* `text/plain` not found - use the first part */ for cur_part in &mail.subparts { - if self.parse_mime_recursive(context, cur_part)? { + if self.parse_mime_recursive(context, cur_part).await? { any_part_added = true; break; } @@ -510,7 +519,7 @@ impl MimeMessage { // we currently do not try to decrypt non-autocrypt messages // at all. If we see an encrypted part, we set // decrypting_failed. - let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody); + let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody).await; let txt = format!("[{}]", msg_body); let mut part = Part::default(); @@ -533,7 +542,7 @@ impl MimeMessage { https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html for background information why we use encrypted+signed) */ if let Some(first) = mail.subparts.iter().next() { - any_part_added = self.parse_mime_recursive(context, first)?; + any_part_added = self.parse_mime_recursive(context, first).await?; } } (mime::MULTIPART, "report") => { @@ -558,7 +567,7 @@ impl MimeMessage { /* eg. `report-type=delivery-status`; maybe we should show them as a little error icon */ if let Some(first) = mail.subparts.iter().next() { - any_part_added = self.parse_mime_recursive(context, first)?; + any_part_added = self.parse_mime_recursive(context, first).await?; } } } @@ -568,7 +577,7 @@ impl MimeMessage { // Add all parts (in fact, AddSinglePartIfKnown() later check if // the parts are really supported) for cur_data in mail.subparts.iter() { - if self.parse_mime_recursive(context, cur_data)? { + if self.parse_mime_recursive(context, cur_data).await? { any_part_added = true; } } @@ -578,7 +587,7 @@ impl MimeMessage { Ok(any_part_added) } - fn add_single_part_if_known( + async fn add_single_part_if_known( &mut self, context: &Context, mail: &mailparse::ParsedMail<'_>, @@ -600,7 +609,8 @@ impl MimeMessage { &raw_mime, &mail.get_body_raw()?, &filename, - ); + ) + .await; } None => { match mime_type.type_() { @@ -652,7 +662,7 @@ impl MimeMessage { Ok(self.parts.len() > old_part_count) } - fn do_add_single_file_part( + async fn do_add_single_file_part( &mut self, context: &Context, msg_type: Viewtype, @@ -685,7 +695,7 @@ impl MimeMessage { /* we have a regular file attachment, write decoded data to new blob object */ - let blob = match BlobObject::create(context, filename, decoded_data) { + let blob = match BlobObject::create(context, filename, decoded_data).await { Ok(blob) => blob, Err(err) => { error!( @@ -829,7 +839,7 @@ impl MimeMessage { } /// Handle reports (only MDNs for now) - pub fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) { + pub async fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) { if self.reports.is_empty() { return; } @@ -840,15 +850,16 @@ impl MimeMessage { { if let Some((chat_id, msg_id)) = message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp) + .await { - context.call_cb(Event::MsgRead { chat_id, msg_id }); + context.emit_event(Event::MsgRead { chat_id, msg_id }); } } } } } -fn update_gossip_peerstates( +async fn update_gossip_peerstates( context: &Context, message_time: i64, mail: &mailparse::ParsedMail<'_>, @@ -865,18 +876,18 @@ fn update_gossip_peerstates( .iter() .any(|info| info.addr == header.addr.to_lowercase()) { - let mut peerstate = Peerstate::from_addr(context, &context.sql, &header.addr); + let mut peerstate = Peerstate::from_addr(context, &header.addr).await; if let Some(ref mut peerstate) = peerstate { peerstate.apply_gossip(header, message_time); - peerstate.save_to_db(&context.sql, false)?; + peerstate.save_to_db(&context.sql, false).await?; } else { let p = Peerstate::from_gossip(context, header, message_time); - p.save_to_db(&context.sql, true)?; + p.save_to_db(&context.sql, true).await?; peerstate = Some(p); } if let Some(peerstate) = peerstate { if peerstate.degrade_event.is_some() { - handle_degrade_event(context, &peerstate)?; + handle_degrade_event(context, &peerstate).await?; } } @@ -1104,21 +1115,25 @@ mod tests { } } - #[test] - fn test_dc_mimeparser_crash() { - let context = dummy_context(); + #[async_std::test] + async fn test_dc_mimeparser_crash() { + let context = dummy_context().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!(mimeparser.get_subject(), None); assert_eq!(mimeparser.parts.len(), 1); } - #[test] - fn test_get_rfc724_mid_exists() { - let context = dummy_context(); + #[async_std::test] + async fn test_get_rfc724_mid_exists() { + let context = dummy_context().await; let raw = include_bytes!("../test-data/message/mail_with_message_id.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( mimeparser.get_rfc724_mid(), @@ -1126,11 +1141,13 @@ mod tests { ); } - #[test] - fn test_get_rfc724_mid_not_exists() { - let context = dummy_context(); + #[async_std::test] + async fn test_get_rfc724_mid_not_exists() { + let context = dummy_context().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!(mimeparser.get_rfc724_mid(), None); } @@ -1182,9 +1199,9 @@ mod tests { ); } - #[test] - fn test_parse_first_addr() { - let context = dummy_context(); + #[async_std::test] + async fn test_parse_first_addr() { + let context = dummy_context().await; let raw = b"From: hello@one.org, world@two.org\n\ Chat-Disposition-Notification-To: wrong\n\ Content-Type: text/plain\n\ @@ -1193,7 +1210,9 @@ mod tests { test1\n\ "; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); let of = &mimeparser.from[0]; assert_eq!(of.addr, "hello@one.org"); @@ -1201,9 +1220,9 @@ mod tests { assert!(mimeparser.chat_disposition_notification_to.is_none()); } - #[test] - fn test_mimeparser_with_context() { - let context = dummy_context(); + #[async_std::test] + async fn test_mimeparser_with_context() { + let context = dummy_context().await; let raw = b"From: hello\n\ Content-Type: multipart/mixed; boundary=\"==break==\";\n\ Subject: outer-subject\n\ @@ -1224,7 +1243,9 @@ mod tests { --==break==--\n\ \n"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); // non-overwritten headers do not bubble up let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap(); @@ -1249,31 +1270,31 @@ mod tests { assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none()); } - #[test] - fn test_mimeparser_with_avatars() { - let t = dummy_context(); + #[async_std::test] + async fn test_mimeparser_with_avatars() { + let t = dummy_context().await; let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap(); assert_eq!(mimeparser.user_avatar, None); assert_eq!(mimeparser.group_avatar, None); let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap(); assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert!(mimeparser.user_avatar.unwrap().is_change()); assert_eq!(mimeparser.group_avatar, None); let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap(); assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete)); assert_eq!(mimeparser.group_avatar, None); let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap(); assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert!(mimeparser.user_avatar.unwrap().is_change()); @@ -1283,16 +1304,18 @@ mod tests { let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); let raw = String::from_utf8_lossy(raw).to_string(); let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:"); - let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes()).unwrap(); + let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes()) + .await + .unwrap(); assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts[0].typ, Viewtype::Image); assert_eq!(mimeparser.user_avatar, None); assert!(mimeparser.group_avatar.unwrap().is_change()); } - #[test] - fn test_mimeparser_message_kml() { - let context = dummy_context(); + #[async_std::test] + async fn test_mimeparser_message_kml() { + let context = dummy_context().await; let raw = b"Chat-Version: 1.0\n\ From: foo \n\ To: bar \n\ @@ -1320,7 +1343,9 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\ --==break==--\n\ ;"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( mimeparser.get_subject(), Some("Location streaming".to_string()) @@ -1333,9 +1358,9 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\ assert_eq!(mimeparser.parts.len(), 1); } - #[test] - fn test_parse_mdn() { - let context = dummy_context(); + #[async_std::test] + async fn test_parse_mdn() { + let context = dummy_context().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -1367,7 +1392,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ "; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( message.get_subject(), Some("Chat: Message opened".to_string()) @@ -1381,9 +1408,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ /// /// RFC 6522 specifically allows MDNs to be nested inside /// multipart MIME messages. - #[test] - fn test_parse_multiple_mdns() { - let context = dummy_context(); + #[async_std::test] + async fn test_parse_multiple_mdns() { + let context = dummy_context().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -1445,7 +1472,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ --outer--\n\ "; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( message.get_subject(), Some("Chat: Message opened".to_string()) @@ -1455,9 +1484,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ assert_eq!(message.reports.len(), 2); } - #[test] - fn test_parse_mdn_with_additional_message_ids() { - let context = dummy_context(); + #[async_std::test] + async fn test_parse_mdn_with_additional_message_ids() { + let context = dummy_context().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -1490,7 +1519,9 @@ Additional-Message-IDs: \n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ "; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( message.get_subject(), Some("Chat: Message opened".to_string()) @@ -1505,9 +1536,9 @@ Additional-Message-IDs: \n\ ); } - #[test] - fn test_parse_inline_attachment() { - let context = dummy_context(); + #[async_std::test] + async fn test_parse_inline_attachment() { + let context = dummy_context().await; let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) From: sender@example.com To: receiver@example.com @@ -1532,7 +1563,9 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== ------=_Part_25_46172632.1581201680436-- "#; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( message.get_subject(), Some("Mail with inline attachment".to_string()) @@ -1543,9 +1576,9 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== assert_eq!(message.parts[0].msg, "Hello!"); } - #[test] - fn parse_inline_image() { - let context = dummy_context(); + #[async_std::test] + async fn parse_inline_image() { + let context = dummy_context().await; let raw = br#"Message-ID: From: foo Subject: example @@ -1579,7 +1612,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= ----11019878869865180-- "#; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!(message.get_subject(), Some("example".to_string())); assert_eq!(message.parts.len(), 1); @@ -1587,9 +1622,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= assert_eq!(message.parts[0].msg, "Test"); } - #[test] - fn parse_thunderbird_html_embedded_image() { - let context = dummy_context(); + #[async_std::test] + async fn parse_thunderbird_html_embedded_image() { + let context = dummy_context().await; let raw = br#"To: Alice From: Bob Subject: Test subject @@ -1649,7 +1684,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= --------------779C1631600DF3DB8C02E53A--"#; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!(message.get_subject(), Some("Test subject".to_string())); assert_eq!(message.parts.len(), 1); @@ -1658,9 +1695,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= } // Outlook specifies filename in the "name" attribute of Content-Type - #[test] - fn parse_outlook_html_embedded_image() { - let context = dummy_context(); + #[async_std::test] + async fn parse_outlook_html_embedded_image() { + let context = dummy_context().await; let raw = br##"From: Anonymous To: Anonymous Subject: Delta Chat is great stuff! @@ -1718,7 +1755,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= ------=_NextPart_000_0003_01D622B3.CA753E60-- "##; - let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); + let message = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); assert_eq!( message.get_subject(), Some("Delta Chat is great stuff!".to_string()) diff --git a/src/oauth2.rs b/src/oauth2.rs index e7a7c8c53..3dd22874f 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -48,7 +48,7 @@ struct Response { scope: Option, } -pub fn dc_get_oauth2_url( +pub async fn dc_get_oauth2_url( context: &Context, addr: impl AsRef, redirect_uri: impl AsRef, @@ -61,6 +61,7 @@ pub fn dc_get_oauth2_url( "oauth2_pending_redirect_uri", Some(redirect_uri.as_ref()), ) + .await .is_err() { return None; @@ -74,21 +75,21 @@ pub fn dc_get_oauth2_url( } } -// The following function may block due http-requests; -// must not be called from the main thread or by the ui! -pub fn dc_get_oauth2_access_token( +pub async fn dc_get_oauth2_access_token( context: &Context, addr: impl AsRef, code: impl AsRef, regenerate: bool, ) -> Option { if let Some(oauth2) = Oauth2::from_address(addr) { - let lock = context.oauth2_critical.clone(); - let _l = lock.lock().unwrap(); + let lock = context.oauth2_mutex.lock().await; // read generated token - if !regenerate && !is_expired(context) { - let access_token = context.sql.get_raw_config(context, "oauth2_access_token"); + if !regenerate && !is_expired(context).await { + let access_token = context + .sql + .get_raw_config(context, "oauth2_access_token") + .await; if access_token.is_some() { // success return access_token; @@ -96,10 +97,14 @@ pub fn dc_get_oauth2_access_token( } // generate new token: build & call auth url - let refresh_token = context.sql.get_raw_config(context, "oauth2_refresh_token"); + let refresh_token = context + .sql + .get_raw_config(context, "oauth2_refresh_token") + .await; let refresh_token_for = context .sql .get_raw_config(context, "oauth2_refresh_token_for") + .await .unwrap_or_else(|| "unset".into()); let (redirect_uri, token_url, update_redirect_uri_on_success) = @@ -109,6 +114,7 @@ pub fn dc_get_oauth2_access_token( context .sql .get_raw_config(context, "oauth2_pending_redirect_uri") + .await .unwrap_or_else(|| "unset".into()), oauth2.init_token, true, @@ -122,6 +128,7 @@ pub fn dc_get_oauth2_access_token( context .sql .get_raw_config(context, "oauth2_redirect_uri") + .await .unwrap_or_else(|| "unset".into()), oauth2.refresh_token, false, @@ -154,10 +161,7 @@ pub fn dc_get_oauth2_access_token( } // ... and POST - let response = reqwest::blocking::Client::new() - .post(post_url) - .form(&post_param) - .send(); + let response = surf::post(post_url).body_form(&post_param); if response.is_err() { warn!( context, @@ -165,19 +169,8 @@ pub fn dc_get_oauth2_access_token( ); return None; } - let response = response.unwrap(); - if !response.status().is_success() { - warn!( - context, - "Unsuccessful response when calling OAuth2 at {}: {:?}", - token_url, - response.status() - ); - return None; - } - // generate new token: parse returned json - let parsed: reqwest::Result = response.json(); + let parsed: Result = response.unwrap().recv_json().await; if parsed.is_err() { warn!( context, @@ -185,7 +178,6 @@ pub fn dc_get_oauth2_access_token( ); return None; } - println!("response: {:?}", &parsed); // update refresh_token if given, typically on the first round, but we update it later as well. let response = parsed.unwrap(); @@ -193,10 +185,12 @@ pub fn dc_get_oauth2_access_token( context .sql .set_raw_config(context, "oauth2_refresh_token", Some(token)) + .await .ok(); context .sql .set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref())) + .await .ok(); } @@ -206,6 +200,7 @@ pub fn dc_get_oauth2_access_token( context .sql .set_raw_config(context, "oauth2_access_token", Some(token)) + .await .ok(); let expires_in = response .expires_in @@ -215,18 +210,22 @@ pub fn dc_get_oauth2_access_token( context .sql .set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in) + .await .ok(); if update_redirect_uri_on_success { context .sql .set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref())) + .await .ok(); } } else { warn!(context, "Failed to find OAuth2 access token"); } + drop(lock); + response.access_token } else { warn!(context, "Internal OAuth2 error: 2"); @@ -235,7 +234,7 @@ pub fn dc_get_oauth2_access_token( } } -pub fn dc_get_oauth2_addr( +pub async fn dc_get_oauth2_addr( context: &Context, addr: impl AsRef, code: impl AsRef, @@ -244,13 +243,14 @@ pub fn dc_get_oauth2_addr( oauth2.get_userinfo?; if let Some(access_token) = - dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false) + dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await { - let addr_out = oauth2.get_addr(context, access_token); + 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) { - oauth2.get_addr(context, access_token) + if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await + { + oauth2.get_addr(context, access_token).await } else { None } @@ -280,7 +280,7 @@ impl Oauth2 { } } - fn get_addr(&self, context: &Context, access_token: impl AsRef) -> Option { + async fn get_addr(&self, context: &Context, access_token: impl AsRef) -> Option { let userinfo_url = self.get_userinfo.unwrap_or_else(|| ""); let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token); @@ -291,50 +291,35 @@ impl Oauth2 { // "verified_email": true, // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - let response = reqwest::blocking::Client::new().get(&userinfo_url).send(); + let response: Result, surf::Error> = + surf::get(userinfo_url).recv_json().await; if response.is_err() { warn!(context, "Error getting userinfo: {:?}", response); return None; } - let response = response.unwrap(); - if !response.status().is_success() { - warn!(context, "Error getting userinfo: {:?}", response.status()); - return None; - } - let parsed: reqwest::Result> = response.json(); - if parsed.is_err() { - warn!( - context, - "Failed to parse userinfo JSON response: {:?}", parsed - ); - return None; - } - if let Ok(response) = parsed { - // CAVE: serde_json::Value.as_str() removes the quotes of json-strings - // but serde_json::Value.to_string() does not! - if let Some(addr) = response.get("email") { - if let Some(s) = addr.as_str() { - Some(s.to_string()) - } else { - warn!(context, "E-mail in userinfo is not a string: {}", addr); - None - } + let parsed = response.unwrap(); + // CAVE: serde_json::Value.as_str() removes the quotes of json-strings + // but serde_json::Value.to_string() does not! + if let Some(addr) = parsed.get("email") { + if let Some(s) = addr.as_str() { + Some(s.to_string()) } else { - warn!(context, "E-mail missing in userinfo."); + warn!(context, "E-mail in userinfo is not a string: {}", addr); None } } else { - warn!(context, "Failed to parse userinfo."); + warn!(context, "E-mail missing in userinfo."); None } } } -fn is_expired(context: &Context) -> bool { +async fn is_expired(context: &Context) -> bool { let expire_timestamp = context .sql .get_raw_config_int64(context, "oauth2_timestamp_expires") + .await .unwrap_or_default(); if expire_timestamp <= 0 { @@ -393,32 +378,32 @@ mod tests { assert_eq!(Oauth2::from_address("hello@web.de"), None); } - #[test] - fn test_dc_get_oauth2_addr() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_dc_get_oauth2_addr() { + let ctx = dummy_context().await; let addr = "dignifiedquire@gmail.com"; let code = "fail"; - let res = dc_get_oauth2_addr(&ctx.ctx, addr, code); + let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await; // this should fail as it is an invalid password assert_eq!(res, None); } - #[test] - fn test_dc_get_oauth2_url() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_dc_get_oauth2_url() { + let ctx = dummy_context().await; let addr = "dignifiedquire@gmail.com"; let redirect_uri = "chat.delta:/com.b44t.messenger"; - let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri); + let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await; assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into())); } - #[test] - fn test_dc_get_oauth2_token() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_dc_get_oauth2_token() { + let ctx = dummy_context().await; let addr = "dignifiedquire@gmail.com"; let code = "fail"; - let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false); + let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await; // this should fail as it is an invalid password assert_eq!(res, None); } diff --git a/src/param.rs b/src/param.rs index 8fe9ce97c..7d72adad9 100644 --- a/src/param.rs +++ b/src/param.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use std::fmt; -use std::path::PathBuf; use std::str; +use async_std::path::PathBuf; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; @@ -275,7 +275,8 @@ impl Params { /// created without copying if the path already referes to a valid /// blob. If so a [BlobObject] will be returned regardless of the /// `create` argument. - pub fn get_blob<'a>( + #[allow(clippy::needless_lifetimes)] + pub async fn get_blob<'a>( &self, key: Param, context: &'a Context, @@ -288,7 +289,7 @@ impl Params { let file = ParamsFile::from_param(context, val)?; let blob = match file { ParamsFile::FsPath(path) => match create { - true => BlobObject::new_from_path(context, path)?, + true => BlobObject::new_from_path(context, path).await?, false => BlobObject::from_path(context, path)?, }, ParamsFile::Blob(blob) => blob, @@ -362,8 +363,8 @@ impl<'a> ParamsFile<'a> { mod tests { use super::*; - use std::fs; - use std::path::Path; + use async_std::fs; + use async_std::path::Path; use crate::test_utils::*; @@ -411,9 +412,9 @@ mod tests { assert_eq!(p1.get(Param::Forwarded).unwrap(), "cli%40deltachat.de"); } - #[test] - fn test_params_file_fs_path() { - let t = dummy_context(); + #[async_std::test] + async fn test_params_file_fs_path() { + let t = dummy_context().await; if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() { assert_eq!(p, Path::new("/foo/bar/baz")); } else { @@ -421,9 +422,9 @@ mod tests { } } - #[test] - fn test_params_file_blob() { - let t = dummy_context(); + #[async_std::test] + async fn test_params_file_blob() { + let t = dummy_context().await; if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() { assert_eq!(b.as_name(), "$BLOBDIR/foo"); } else { @@ -432,28 +433,33 @@ mod tests { } // Tests for Params::get_file(), Params::get_path() and Params::get_blob(). - #[test] - fn test_params_get_fileparam() { - let t = dummy_context(); + #[async_std::test] + async fn test_params_get_fileparam() { + let t = dummy_context().await; let fname = t.dir.path().join("foo"); let mut p = Params::new(); p.set(Param::File, fname.to_str().unwrap()); let file = p.get_file(Param::File, &t.ctx).unwrap().unwrap(); - assert_eq!(file, ParamsFile::FsPath(fname.clone())); + assert_eq!(file, ParamsFile::FsPath(fname.clone().into())); - let path = p.get_path(Param::File, &t.ctx).unwrap().unwrap(); + let path: PathBuf = p.get_path(Param::File, &t.ctx).unwrap().unwrap(); + let fname: PathBuf = fname.into(); assert_eq!(path, fname); // Blob does not exist yet, expect BlobError. - let err = p.get_blob(Param::File, &t.ctx, false).unwrap_err(); + let err = p.get_blob(Param::File, &t.ctx, false).await.unwrap_err(); match err { BlobError::WrongBlobdir { .. } => (), _ => panic!("wrong error type/variant: {:?}", err), } - fs::write(fname, b"boo").unwrap(); - let blob = p.get_blob(Param::File, &t.ctx, true).unwrap().unwrap(); + fs::write(fname, b"boo").await.unwrap(); + let blob = p + .get_blob(Param::File, &t.ctx, true) + .await + .unwrap() + .unwrap(); assert_eq!( blob, BlobObject::from_name(&t.ctx, "foo".to_string()).unwrap() @@ -462,7 +468,11 @@ mod tests { // Blob in blobdir, expect blob. let bar = t.ctx.get_blobdir().join("bar"); p.set(Param::File, bar.to_str().unwrap()); - let blob = p.get_blob(Param::File, &t.ctx, false).unwrap().unwrap(); + let blob = p + .get_blob(Param::File, &t.ctx, false) + .await + .unwrap() + .unwrap(); assert_eq!( blob, BlobObject::from_name(&t.ctx, "bar".to_string()).unwrap() @@ -471,6 +481,10 @@ mod tests { p.remove(Param::File); assert!(p.get_file(Param::File, &t.ctx).unwrap().is_none()); assert!(p.get_path(Param::File, &t.ctx).unwrap().is_none()); - assert!(p.get_blob(Param::File, &t.ctx, false).unwrap().is_none()); + assert!(p + .get_blob(Param::File, &t.ctx, false) + .await + .unwrap() + .is_none()); } } diff --git a/src/peerstate.rs b/src/peerstate.rs index e2a17e0ce..fc36020a6 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -9,7 +9,7 @@ use crate::aheader::*; use crate::constants::*; use crate::context::Context; use crate::key::{Key, SignedPublicKey}; -use crate::sql::{self, Sql}; +use crate::sql::Sql; #[derive(Debug)] pub enum PeerstateKeyType { @@ -144,12 +144,16 @@ impl<'a> Peerstate<'a> { res } - pub fn from_addr(context: &'a Context, _sql: &Sql, addr: &str) -> Option { + pub async fn from_addr(context: &'a Context, addr: &str) -> Option> { let 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, &[addr]) + Self::from_stmt(context, query, paramsv![addr]).await } - pub fn from_fingerprint(context: &'a Context, _sql: &Sql, fingerprint: &str) -> Option { + pub async fn from_fingerprint( + context: &'a Context, + _sql: &Sql, + fingerprint: &str, + ) -> Option> { let 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 \ @@ -161,15 +165,16 @@ impl<'a> Peerstate<'a> { Self::from_stmt( context, query, - params![fingerprint, fingerprint, fingerprint], + paramsv![fingerprint, fingerprint, fingerprint], ) + .await } - fn from_stmt

(context: &'a Context, query: &str, params: P) -> Option - where - P: IntoIterator, - P::Item: rusqlite::ToSql, - { + async fn from_stmt( + context: &'a Context, + query: &str, + params: Vec<&dyn crate::ToSql>, + ) -> Option> { context .sql .query_row(query, params, |row| { @@ -215,18 +220,19 @@ impl<'a> Peerstate<'a> { res.public_key = row .get(4) .ok() - .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public)); + .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public).ok()); res.gossip_key = row .get(6) .ok() - .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public)); + .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public).ok()); res.verified_key = row .get(9) .ok() - .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public)); + .and_then(|blob: Vec| Key::from_slice(&blob, KeyType::Public).ok()); Ok(res) }) + .await .ok() } @@ -361,6 +367,15 @@ impl<'a> Peerstate<'a> { } } + pub fn take_key(mut self, min_verified: PeerstateVerifiedStatus) -> Option { + match min_verified { + PeerstateVerifiedStatus::BidirectVerified => self.verified_key.take(), + PeerstateVerifiedStatus::Unverified => { + self.public_key.take().or_else(|| self.gossip_key.take()) + } + } + } + pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> { match min_verified { PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(), @@ -409,52 +424,48 @@ impl<'a> Peerstate<'a> { } } - pub fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> { + pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> { if create { - sql::execute( - self.context, - sql, + sql.execute( "INSERT INTO acpeerstates (addr) VALUES(?);", - params![self.addr], - )?; + paramsv![self.addr], + ) + .await?; } if self.to_save == Some(ToSave::All) || create { - sql::execute( - self.context, - sql, + sql.execute( "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=?;", - params![ + 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, - &self.gossip_key_fingerprint, + self.public_key_fingerprint, + self.gossip_key_fingerprint, self.verified_key.as_ref().map(|k| k.to_bytes()), - &self.verified_key_fingerprint, - &self.addr, + self.verified_key_fingerprint, + self.addr, ], - )?; + ).await?; } else if self.to_save == Some(ToSave::Timestamps) { - sql::execute( - self.context, - sql, + sql.execute( "UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ WHERE addr=?;", - params![ + paramsv![ self.last_seen, self.last_seen_autocrypt, self.gossip_timestamp, - &self.addr + self.addr ], - )?; + ) + .await?; } Ok(()) @@ -479,9 +490,9 @@ mod tests { use pretty_assertions::assert_eq; use tempfile::TempDir; - #[test] - fn test_peerstate_save_to_db() { - let ctx = crate::test_utils::dummy_context(); + #[async_std::test] + async fn test_peerstate_save_to_db() { + let ctx = crate::test_utils::dummy_context().await; let addr = "hello@mail.com"; let pub_key = crate::key::Key::from(alice_keypair().public); @@ -504,11 +515,12 @@ mod tests { }; assert!( - peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), + peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), "failed to save to db" ); - let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr) + let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr) + .await .expect("failed to load peerstate from db"); // clear to_save, as that is not persissted @@ -516,13 +528,14 @@ mod tests { assert_eq!(peerstate, peerstate_new); let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint()) + .await .expect("failed to load peerstate from db"); assert_eq!(peerstate, peerstate_new2); } - #[test] - fn test_peerstate_double_create() { - let ctx = crate::test_utils::dummy_context(); + #[async_std::test] + async fn test_peerstate_double_create() { + let ctx = crate::test_utils::dummy_context().await; let addr = "hello@mail.com"; let pub_key = crate::key::Key::from(alice_keypair().public); @@ -544,18 +557,18 @@ mod tests { }; assert!( - peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), + peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), "failed to save" ); assert!( - peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), + peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), "double-call with create failed" ); } - #[test] - fn test_peerstate_with_empty_gossip_key_save_to_db() { - let ctx = crate::test_utils::dummy_context(); + #[async_std::test] + async fn test_peerstate_with_empty_gossip_key_save_to_db() { + let ctx = crate::test_utils::dummy_context().await; let addr = "hello@mail.com"; let pub_key = crate::key::Key::from(alice_keypair().public); @@ -578,11 +591,12 @@ mod tests { }; assert!( - peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), + peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), "failed to save" ); - let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr) + let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr) + .await .expect("failed to load peerstate from db"); // clear to_save, as that is not persissted diff --git a/src/pgp.rs b/src/pgp.rs index 287c7b0bf..fef20379d 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -238,124 +238,140 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option, + public_keys_for_encryption: Keyring, + private_key_for_signing: Option, ) -> Result { let lit_msg = Message::new_literal_bytes("", plain); - let pkeys: Vec = public_keys_for_encryption - .keys() - .iter() - .filter_map(|key| { - key.as_ref() + + async_std::task::spawn_blocking(move || { + let pkeys: Vec = public_keys_for_encryption + .keys() + .iter() + .filter_map(|key| key.try_into().ok().and_then(select_pk_for_encryption)) + .collect(); + let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect(); + + let mut rng = thread_rng(); + + // TODO: measure time + let encrypted_msg = if let Some(ref private_key) = private_key_for_signing { + let skey: &SignedSecretKey = private_key .try_into() - .ok() - .and_then(select_pk_for_encryption) - }) - .collect(); - let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect(); + .map_err(|_| format_err!("Invalid private key"))?; - let mut rng = thread_rng(); + lit_msg + .sign(skey, || "".into(), Default::default()) + .and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB)) + .and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)) + } else { + lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs) + }; - // TODO: measure time - let encrypted_msg = if let Some(private_key) = private_key_for_signing { - let skey: &SignedSecretKey = private_key - .try_into() - .map_err(|_| format_err!("Invalid private key"))?; + let msg = encrypted_msg?; + let encoded_msg = msg.to_armored_string(None)?; - lit_msg - .sign(skey, || "".into(), Default::default()) - .and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB)) - .and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)) - } else { - lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs) - }; - - let msg = encrypted_msg?; - let encoded_msg = msg.to_armored_string(None)?; - - Ok(encoded_msg) + Ok(encoded_msg) + }) + .await } #[allow(clippy::implicit_hasher)] -pub fn pk_decrypt( - ctext: &[u8], - private_keys_for_decryption: &Keyring, - public_keys_for_validation: &Keyring, +pub async fn pk_decrypt( + ctext: Vec, + private_keys_for_decryption: Keyring, + public_keys_for_validation: Keyring, ret_signature_fingerprints: Option<&mut HashSet>, ) -> Result> { - let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?; - let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption - .keys() - .iter() - .filter_map(|key| { - let k: &Key = &key; - k.try_into().ok() - }) - .collect(); + let msgs = async_std::task::spawn_blocking(move || { + let cursor = Cursor::new(ctext); + let (msg, _) = Message::from_armor_single(cursor)?; + + let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption + .keys() + .iter() + .filter_map(|key| key.try_into().ok()) + .collect(); + + let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; + decryptor.collect::>>() + }) + .await?; - let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; - let msgs = decryptor.collect::>>()?; ensure!(!msgs.is_empty(), "No valid messages found"); - let dec_msg = &msgs[0]; + let content = match msgs[0].get_content()? { + Some(content) => content, + None => bail!("Decrypted message is empty"), + }; if let Some(ret_signature_fingerprints) = ret_signature_fingerprints { - if !public_keys_for_validation.keys().is_empty() { - let pkeys: Vec<&SignedPublicKey> = public_keys_for_validation - .keys() - .iter() - .filter_map(|key| { - let k: &Key = &key; - k.try_into().ok() - }) - .collect(); + if !public_keys_for_validation.is_empty() { + let fingerprints = async_std::task::spawn_blocking(move || { + let dec_msg = &msgs[0]; - for pkey in &pkeys { - if dec_msg.verify(&pkey.primary_key).is_ok() { - let fp = hex::encode_upper(pkey.fingerprint()); - ret_signature_fingerprints.insert(fp); + let pkeys = public_keys_for_validation + .keys() + .iter() + .filter_map(|key| -> Option<&SignedPublicKey> { key.try_into().ok() }); + + let mut fingerprints = Vec::new(); + for pkey in pkeys { + if dec_msg.verify(&pkey.primary_key).is_ok() { + let fp = hex::encode_upper(pkey.fingerprint()); + fingerprints.push(fp); + } } - } + fingerprints + }) + .await; + + ret_signature_fingerprints.extend(fingerprints); } } - match dec_msg.get_content()? { - Some(content) => Ok(content), - None => bail!("Decrypted message is empty"), - } + Ok(content) } /// Symmetric encryption. -pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { - let mut rng = thread_rng(); +pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { let lit_msg = Message::new_literal_bytes("", plain); + let passphrase = passphrase.to_string(); - let s2k = StringToKey::new_default(&mut rng); - let msg = - lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase.into())?; + async_std::task::spawn_blocking(move || { + let mut rng = thread_rng(); + let s2k = StringToKey::new_default(&mut rng); + let msg = + lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?; - let encoded_msg = msg.to_armored_string(None)?; + let encoded_msg = msg.to_armored_string(None)?; - Ok(encoded_msg) + Ok(encoded_msg) + }) + .await } /// Symmetric decryption. -pub fn symm_decrypt( +pub async fn symm_decrypt( passphrase: &str, ctext: T, ) -> Result> { let (enc_msg, _) = Message::from_armor_single(ctext)?; - let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?; - let msgs = decryptor.collect::>>()?; - ensure!(!msgs.is_empty(), "No valid messages found"); + let passphrase = passphrase.to_string(); + async_std::task::spawn_blocking(move || { + let decryptor = enc_msg.decrypt_with_password(|| passphrase)?; - match msgs[0].get_content()? { - Some(content) => Ok(content), - None => bail!("Decrypted message is empty"), - } + let msgs = decryptor.collect::>>()?; + ensure!(!msgs.is_empty(), "No valid messages found"); + + match msgs[0].get_content()? { + Some(content) => Ok(content), + None => bail!("Decrypted message is empty"), + } + }) + .await } #[cfg(test)] @@ -437,17 +453,17 @@ mod tests { /// A cyphertext encrypted to Alice & Bob, signed by Alice. static ref CTEXT_SIGNED: String = { let mut keyring = Keyring::default(); - keyring.add_owned(KEYS.alice_public.clone()); - keyring.add_ref(&KEYS.bob_public); - pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap() + keyring.add(KEYS.alice_public.clone()); + keyring.add(KEYS.bob_public.clone()); + smol::block_on(pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))).unwrap() }; /// A cyphertext encrypted to Alice & Bob, not signed. static ref CTEXT_UNSIGNED: String = { let mut keyring = Keyring::default(); - keyring.add_owned(KEYS.alice_public.clone()); - keyring.add_ref(&KEYS.bob_public); - pk_encrypt(CLEARTEXT, &keyring, None).unwrap() + keyring.add(KEYS.alice_public.clone()); + keyring.add(KEYS.bob_public.clone()); + smol::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap() }; } @@ -463,20 +479,21 @@ mod tests { assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----")); } - #[test] - fn test_decrypt_singed() { + #[async_std::test] + async fn test_decrypt_singed() { // Check decrypting as Alice let mut decrypt_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.alice_secret); + decrypt_keyring.add(KEYS.alice_secret.clone()); let mut sig_check_keyring = Keyring::default(); - sig_check_keyring.add_ref(&KEYS.alice_public); + sig_check_keyring.add(KEYS.alice_public.clone()); let mut valid_signatures: HashSet = Default::default(); let plain = pk_decrypt( - CTEXT_SIGNED.as_bytes(), - &decrypt_keyring, - &sig_check_keyring, + CTEXT_SIGNED.as_bytes().to_vec(), + decrypt_keyring, + sig_check_keyring, Some(&mut valid_signatures), ) + .await .map_err(|err| println!("{:?}", err)) .unwrap(); assert_eq!(plain, CLEARTEXT); @@ -484,89 +501,94 @@ mod tests { // Check decrypting as Bob let mut decrypt_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.bob_secret); + decrypt_keyring.add(KEYS.bob_secret.clone()); let mut sig_check_keyring = Keyring::default(); - sig_check_keyring.add_ref(&KEYS.alice_public); + sig_check_keyring.add(KEYS.alice_public.clone()); let mut valid_signatures: HashSet = Default::default(); let plain = pk_decrypt( - CTEXT_SIGNED.as_bytes(), - &decrypt_keyring, - &sig_check_keyring, + CTEXT_SIGNED.as_bytes().to_vec(), + decrypt_keyring, + sig_check_keyring, Some(&mut valid_signatures), ) + .await .map_err(|err| println!("{:?}", err)) .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); } - #[test] - fn test_decrypt_no_sig_check() { + #[async_std::test] + async fn test_decrypt_no_sig_check() { let mut keyring = Keyring::default(); - keyring.add_ref(&KEYS.alice_secret); + keyring.add(KEYS.alice_secret.clone()); let empty_keyring = Keyring::default(); let mut valid_signatures: HashSet = Default::default(); let plain = pk_decrypt( - CTEXT_SIGNED.as_bytes(), - &keyring, - &empty_keyring, + CTEXT_SIGNED.as_bytes().to_vec(), + keyring, + empty_keyring, Some(&mut valid_signatures), ) + .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } - #[test] - fn test_decrypt_signed_no_key() { + #[async_std::test] + async fn test_decrypt_signed_no_key() { // The validation does not have the public key of the signer. let mut decrypt_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.bob_secret); + decrypt_keyring.add(KEYS.bob_secret.clone()); let mut sig_check_keyring = Keyring::default(); - sig_check_keyring.add_ref(&KEYS.bob_public); + sig_check_keyring.add(KEYS.bob_public.clone()); let mut valid_signatures: HashSet = Default::default(); let plain = pk_decrypt( - CTEXT_SIGNED.as_bytes(), - &decrypt_keyring, - &sig_check_keyring, + CTEXT_SIGNED.as_bytes().to_vec(), + decrypt_keyring, + sig_check_keyring, Some(&mut valid_signatures), ) + .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } - #[test] - fn test_decrypt_unsigned() { + #[async_std::test] + async fn test_decrypt_unsigned() { let mut decrypt_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.bob_secret); + decrypt_keyring.add(KEYS.bob_secret.clone()); let sig_check_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.alice_public); + decrypt_keyring.add(KEYS.alice_public.clone()); let mut valid_signatures: HashSet = Default::default(); let plain = pk_decrypt( - CTEXT_UNSIGNED.as_bytes(), - &decrypt_keyring, - &sig_check_keyring, + CTEXT_UNSIGNED.as_bytes().to_vec(), + decrypt_keyring, + sig_check_keyring, Some(&mut valid_signatures), ) + .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } - #[test] - fn test_decrypt_signed_no_sigret() { + #[async_std::test] + async fn test_decrypt_signed_no_sigret() { // Check decrypting signed cyphertext without providing the HashSet for signatures. let mut decrypt_keyring = Keyring::default(); - decrypt_keyring.add_ref(&KEYS.bob_secret); + decrypt_keyring.add(KEYS.bob_secret.clone()); let mut sig_check_keyring = Keyring::default(); - sig_check_keyring.add_ref(&KEYS.alice_public); + sig_check_keyring.add(KEYS.alice_public.clone()); let plain = pk_decrypt( - CTEXT_SIGNED.as_bytes(), - &decrypt_keyring, - &sig_check_keyring, + CTEXT_SIGNED.as_bytes().to_vec(), + decrypt_keyring, + sig_check_keyring, None, ) + .await .unwrap(); assert_eq!(plain, CLEARTEXT); } diff --git a/src/qr.rs b/src/qr.rs index 6c4f21ce5..ff8487232 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -2,7 +2,6 @@ use lazy_static::lazy_static; use percent_encoding::percent_decode_str; -use reqwest::Url; use serde::Deserialize; use crate::chat; @@ -44,23 +43,23 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool { /// Check a scanned QR code. /// The function should be called after a QR code is scanned. /// The function takes the raw text scanned and checks what can be done with it. -pub fn check_qr(context: &Context, qr: impl AsRef) -> Lot { +pub async fn check_qr(context: &Context, qr: impl AsRef) -> Lot { let qr = qr.as_ref(); info!(context, "Scanned QR code: {}", qr); if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) { - decode_openpgp(context, qr) + decode_openpgp(context, qr).await } else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) { decode_account(context, qr) } else if qr.starts_with(MAILTO_SCHEME) { - decode_mailto(context, qr) + decode_mailto(context, qr).await } else if qr.starts_with(SMTP_SCHEME) { - decode_smtp(context, qr) + decode_smtp(context, qr).await } else if qr.starts_with(MATMSG_SCHEME) { - decode_matmsg(context, qr) + decode_matmsg(context, qr).await } else if qr.starts_with(VCARD_SCHEME) { - decode_vcard(context, qr) + decode_vcard(context, qr).await } else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) { Lot::from_url(qr) } else { @@ -70,7 +69,7 @@ pub fn check_qr(context: &Context, qr: impl AsRef) -> 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` -fn decode_openpgp(context: &Context, qr: &str) -> Lot { +async fn decode_openpgp(context: &Context, qr: &str) -> Lot { let payload = &qr[OPENPGP4FPR_SCHEME.len()..]; let (fingerprint, fragment) = match payload.find('#').map(|offset| { @@ -136,15 +135,10 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot { return format_err!("Bad fingerprint length in QR code").into(); } - println!( - "{:?} {:?} {:?} {:?} {:?} {:?} {:?}", - addr, name, invitenumber, auth, grpid, grpname, fingerprint - ); - let mut lot = Lot::new(); // retrieve known state for this fingerprint - let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint); + let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await; if invitenumber.is_none() || auth.is_none() { if let Some(peerstate) = peerstate { @@ -156,13 +150,15 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot { peerstate.addr.clone(), Origin::UnhandledQrScan, ) + .await .map(|(id, _)| id) .unwrap_or_default(); let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop) + .await .unwrap_or_default(); - chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)); + chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await; } else { lot.state = LotState::QrFprWithoutAddr; lot.text1 = Some(dc_format_fingerprint(&fingerprint)); @@ -176,6 +172,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot { lot.state = LotState::QrAskVerifyContact; } lot.id = Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan) + .await .map(|(id, _)| id) .unwrap_or_default(); @@ -195,7 +192,7 @@ fn decode_account(_context: &Context, qr: &str) -> Lot { let mut lot = Lot::new(); - if let Ok(url) = Url::parse(payload) { + if let Ok(url) = url::Url::parse(payload) { if url.scheme() == "https" { lot.state = LotState::QrAccount; lot.text1 = url.host_str().map(|x| x.to_string()); @@ -220,31 +217,22 @@ struct CreateAccountResponse { /// take a qr of the type DC_QR_ACCOUNT, parse it's parameters, /// download additional information from the contained url and set the parameters. /// on success, a configure::configure() should be able to log in to the account -pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> { +pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> { let url_str = &qr[DCACCOUNT_SCHEME.len()..]; - let response = reqwest::blocking::Client::new().post(url_str).send(); + let response: Result = + surf::post(url_str).recv_json().await; if response.is_err() { bail!("Cannot create account, request to {} failed", url_str); } - let response = response.unwrap(); - if !response.status().is_success() { - bail!("Request to {} unsuccessful: {:?}", url_str, response); - } + let parsed = response.unwrap(); - let parsed: reqwest::Result = response.json(); - if parsed.is_err() { - bail!( - "Failed to parse JSON response from {}: error: {:?}", - url_str, - parsed - ); - } - println!("response: {:?}", &parsed); - let parsed = parsed.unwrap(); - - context.set_config(Config::Addr, Some(&parsed.email))?; - context.set_config(Config::MailPw, Some(&parsed.password))?; + context + .set_config(Config::Addr, Some(&parsed.email)) + .await?; + context + .set_config(Config::MailPw, Some(&parsed.password)) + .await?; Ok(()) } @@ -252,7 +240,7 @@ pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> { /// Extract address for the mailto scheme. /// /// Scheme: `mailto:addr...?subject=...&body=..` -fn decode_mailto(context: &Context, qr: &str) -> Lot { +async fn decode_mailto(context: &Context, qr: &str) -> Lot { let payload = &qr[MAILTO_SCHEME.len()..]; let addr = if let Some(query_index) = payload.find('?') { @@ -267,13 +255,13 @@ fn decode_mailto(context: &Context, qr: &str) -> Lot { }; let name = "".to_string(); - Lot::from_address(context, name, addr) + Lot::from_address(context, name, addr).await } /// Extract address for the smtp scheme. /// /// Scheme: `SMTP:addr...:subject...:body...` -fn decode_smtp(context: &Context, qr: &str) -> Lot { +async fn decode_smtp(context: &Context, qr: &str) -> Lot { let payload = &qr[SMTP_SCHEME.len()..]; let addr = if let Some(query_index) = payload.find(':') { @@ -287,7 +275,7 @@ fn decode_smtp(context: &Context, qr: &str) -> Lot { Err(err) => return err.into(), }; let name = "".to_string(); - Lot::from_address(context, name, addr) + Lot::from_address(context, name, addr).await } /// Extract address for the matmsg scheme. @@ -295,7 +283,7 @@ fn decode_smtp(context: &Context, qr: &str) -> Lot { /// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;` /// /// There may or may not be linebreaks after the fields. -fn decode_matmsg(context: &Context, qr: &str) -> Lot { +async fn decode_matmsg(context: &Context, qr: &str) -> Lot { // Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field. // we ignore this case. let addr = if let Some(to_index) = qr.find("TO:") { @@ -315,7 +303,7 @@ fn decode_matmsg(context: &Context, qr: &str) -> Lot { }; let name = "".to_string(); - Lot::from_address(context, name, addr) + Lot::from_address(context, name, addr).await } lazy_static! { @@ -328,7 +316,7 @@ lazy_static! { /// Extract address for the matmsg scheme. /// /// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;:addr...; -fn decode_vcard(context: &Context, qr: &str) -> Lot { +async fn decode_vcard(context: &Context, qr: &str) -> Lot { let name = VCARD_NAME_RE .captures(qr) .map(|caps| { @@ -348,7 +336,7 @@ fn decode_vcard(context: &Context, qr: &str) -> Lot { return format_err!("Bad e-mail address").into(); }; - Lot::from_address(context, name, addr) + Lot::from_address(context, name, addr).await } impl Lot { @@ -368,10 +356,10 @@ impl Lot { l } - pub fn from_address(context: &Context, name: String, addr: String) -> Self { + pub async fn from_address(context: &Context, name: String, addr: String) -> Self { let mut l = Lot::new(); l.state = LotState::QrAddr; - l.id = match Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan) { + l.id = match Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await { Ok((id, _)) => id, Err(err) => return err.into(), }; @@ -397,11 +385,11 @@ mod tests { use crate::test_utils::dummy_context; - #[test] - fn test_decode_http() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_http() { + let ctx = dummy_context().await; - let res = check_qr(&ctx.ctx, "http://www.hello.com"); + let res = check_qr(&ctx.ctx, "http://www.hello.com").await; assert_eq!(res.get_state(), LotState::QrUrl); assert_eq!(res.get_id(), 0); @@ -409,11 +397,11 @@ mod tests { assert!(res.get_text2().is_none()); } - #[test] - fn test_decode_https() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_https() { + let ctx = dummy_context().await; - let res = check_qr(&ctx.ctx, "https://www.hello.com"); + let res = check_qr(&ctx.ctx, "https://www.hello.com").await; assert_eq!(res.get_state(), LotState::QrUrl); assert_eq!(res.get_id(), 0); @@ -421,11 +409,11 @@ mod tests { assert!(res.get_text2().is_none()); } - #[test] - fn test_decode_text() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_text() { + let ctx = dummy_context().await; - let res = check_qr(&ctx.ctx, "I am so cool"); + let res = check_qr(&ctx.ctx, "I am so cool").await; assert_eq!(res.get_state(), LotState::QrText); assert_eq!(res.get_id(), 0); @@ -433,88 +421,90 @@ mod tests { assert!(res.get_text2().is_none()); } - #[test] - fn test_decode_vcard() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_vcard() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD" - ); + ).await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAddr); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_name(), "First Last"); } - #[test] - fn test_decode_matmsg() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_matmsg() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;", - ); + ) + .await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAddr); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "stress@test.local"); } - #[test] - fn test_decode_mailto() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_mailto() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "mailto:stress@test.local?subject=hello&body=world", - ); + ) + .await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAddr); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "stress@test.local"); - let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org"); + let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await; assert_eq!(res.get_state(), LotState::QrAddr); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "no-questionmark@example.org"); - let res = check_qr(&ctx.ctx, "mailto:no-addr"); + let res = check_qr(&ctx.ctx, "mailto:no-addr").await; assert_eq!(res.get_state(), LotState::QrError); assert!(res.get_text1().is_some()); } - #[test] - fn test_decode_smtp() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_smtp() { + let ctx = dummy_context().await; - let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld"); + let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAddr); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "stress@test.local"); } - #[test] - fn test_decode_openpgp_group() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_openpgp_group() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" - ); + ).await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAskVerifyGroup); @@ -525,25 +515,25 @@ mod tests { let res = check_qr( &ctx.ctx, "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" - ); + ).await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAskVerifyGroup); assert_ne!(res.get_id(), 0); assert_eq!(res.get_text1().unwrap(), "test ? test !"); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "cli@deltachat.de"); } - #[test] - fn test_decode_openpgp_secure_join() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_openpgp_secure_join() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" - ); + ).await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAskVerifyContact); @@ -553,25 +543,26 @@ mod tests { let res = check_qr( &ctx.ctx, "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" - ); + ).await; println!("{:?}", res); assert_eq!(res.get_state(), LotState::QrAskVerifyContact); assert_ne!(res.get_id(), 0); - let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); + let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap(); assert_eq!(contact.get_addr(), "cli@deltachat.de"); assert_eq!(contact.get_name(), "Jörn P. P."); } - #[test] - fn test_decode_openpgp_without_addr() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_openpgp_without_addr() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "OPENPGP4FPR:1234567890123456789012345678901234567890", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrFprWithoutAddr); assert_eq!( res.get_text1().unwrap(), @@ -584,7 +575,8 @@ mod tests { let res = check_qr( &ctx.ctx, "openpgp4fpr:1234567890123456789012345678901234567890", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrFprWithoutAddr); assert_eq!( res.get_text1().unwrap(), @@ -592,19 +584,20 @@ mod tests { ); assert_eq!(res.get_id(), 0); - let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890"); + let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await; assert_eq!(res.get_state(), LotState::QrError); assert_eq!(res.get_id(), 0); } - #[test] - fn test_decode_account() { - let ctx = dummy_context(); + #[async_std::test] + async fn test_decode_account() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrAccount); assert_eq!(res.get_text1().unwrap(), "example.org"); @@ -612,19 +605,20 @@ mod tests { let res = check_qr( &ctx.ctx, "dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrAccount); assert_eq!(res.get_text1().unwrap(), "example.org"); } - #[test] - fn test_decode_account_bad_scheme() { - let ctx = dummy_context(); - + #[async_std::test] + async fn test_decode_account_bad_scheme() { + let ctx = dummy_context().await; let res = check_qr( &ctx.ctx, "DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrError); assert!(res.get_text1().is_some()); @@ -632,7 +626,8 @@ mod tests { let res = check_qr( &ctx.ctx, "dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ); + ) + .await; assert_eq!(res.get_state(), LotState::QrError); assert!(res.get_text1().is_some()); } diff --git a/src/scheduler.rs b/src/scheduler.rs new file mode 100644 index 000000000..e4076a6c4 --- /dev/null +++ b/src/scheduler.rs @@ -0,0 +1,592 @@ +use async_std::prelude::*; +use async_std::sync::{channel, Receiver, Sender}; +use async_std::task; + +use crate::context::Context; +use crate::imap::Imap; +use crate::job::{self, Thread}; +use crate::smtp::Smtp; + +pub(crate) struct StopToken; + +/// Job and connection scheduler. +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum Scheduler { + Stopped, + Running { + inbox: ImapConnectionState, + inbox_handle: Option>, + mvbox: ImapConnectionState, + mvbox_handle: Option>, + sentbox: ImapConnectionState, + sentbox_handle: Option>, + smtp: SmtpConnectionState, + smtp_handle: Option>, + }, +} + +impl Context { + /// Indicate that the network likely has come back. + pub async fn maybe_network(&self) { + self.scheduler.read().await.maybe_network().await; + } + + pub(crate) async fn interrupt_inbox(&self, probe_network: bool) { + self.scheduler + .read() + .await + .interrupt_inbox(probe_network) + .await; + } + + pub(crate) async fn interrupt_sentbox(&self, probe_network: bool) { + self.scheduler + .read() + .await + .interrupt_sentbox(probe_network) + .await; + } + + pub(crate) async fn interrupt_mvbox(&self, probe_network: bool) { + self.scheduler + .read() + .await + .interrupt_mvbox(probe_network) + .await; + } + + pub(crate) async fn interrupt_smtp(&self, probe_network: bool) { + self.scheduler + .read() + .await + .interrupt_smtp(probe_network) + .await; + } +} + +async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) { + use futures::future::FutureExt; + + info!(ctx, "starting inbox loop"); + let ImapConnectionHandlers { + mut connection, + stop_receiver, + shutdown_sender, + } = inbox_handlers; + + let ctx1 = ctx.clone(); + let fut = async move { + started.send(()).await; + let ctx = ctx1; + if let Err(err) = connection.connect_configured(&ctx).await { + error!(ctx, "{}", err); + return; + } + + // track number of continously executed jobs + let mut jobs_loaded = 0; + let mut probe_network = false; + loop { + match job::load_next(&ctx, Thread::Imap, probe_network).await { + Some(job) if jobs_loaded <= 20 => { + jobs_loaded += 1; + job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await; + probe_network = false; + } + Some(job) => { + // Let the fetch run, but return back to the job afterwards. + info!(ctx, "postponing imap-job {} to run fetch...", job); + jobs_loaded = 0; + fetch(&ctx, &mut connection).await; + } + None => { + jobs_loaded = 0; + probe_network = fetch_idle(&ctx, &mut connection).await; + } + } + } + }; + + stop_receiver + .recv() + .map(|_| { + info!(ctx, "shutting down inbox loop"); + }) + .race(fut) + .await; + shutdown_sender.send(()).await; +} + +async fn fetch(ctx: &Context, connection: &mut Imap) { + match get_watch_folder(&ctx, "configured_inbox_folder").await { + Some(watch_folder) => { + // fetch + connection + .fetch(&ctx, &watch_folder) + .await + .unwrap_or_else(|err| { + error!(ctx, "{}", err); + }); + } + None => { + warn!(ctx, "Can not fetch inbox folder, not set"); + connection.fake_idle(&ctx, None).await; + } + } +} + +async fn fetch_idle(ctx: &Context, connection: &mut Imap) -> bool { + match get_watch_folder(&ctx, "configured_inbox_folder").await { + Some(watch_folder) => { + // fetch + connection + .fetch(&ctx, &watch_folder) + .await + .unwrap_or_else(|err| { + error!(ctx, "{}", err); + }); + + // idle + if connection.can_idle() { + connection + .idle(&ctx, Some(watch_folder)) + .await + .unwrap_or_else(|err| { + error!(ctx, "{}", err); + false + }) + } else { + connection.fake_idle(&ctx, Some(watch_folder)).await + } + } + None => { + warn!(ctx, "Can not watch inbox folder, not set"); + connection.fake_idle(&ctx, None).await + } + } +} + +async fn simple_imap_loop( + ctx: Context, + started: Sender<()>, + inbox_handlers: ImapConnectionHandlers, + folder: impl AsRef, +) { + use futures::future::FutureExt; + + info!(ctx, "starting simple loop for {}", folder.as_ref()); + let ImapConnectionHandlers { + mut connection, + stop_receiver, + shutdown_sender, + } = inbox_handlers; + + let ctx1 = ctx.clone(); + + let fut = async move { + started.send(()).await; + let ctx = ctx1; + if let Err(err) = connection.connect_configured(&ctx).await { + error!(ctx, "{}", err); + return; + } + + loop { + match get_watch_folder(&ctx, folder.as_ref()).await { + Some(watch_folder) => { + // fetch + connection + .fetch(&ctx, &watch_folder) + .await + .unwrap_or_else(|err| { + error!(ctx, "{}", err); + }); + + // idle + if connection.can_idle() { + connection + .idle(&ctx, Some(watch_folder)) + .await + .unwrap_or_else(|err| { + error!(ctx, "{}", err); + false + }); + } else { + connection.fake_idle(&ctx, Some(watch_folder)).await; + } + } + None => { + warn!( + &ctx, + "No watch folder found for {}, skipping", + folder.as_ref() + ); + connection.fake_idle(&ctx, None).await; + } + } + } + }; + + stop_receiver + .recv() + .map(|_| { + info!(ctx, "shutting down simple loop"); + }) + .race(fut) + .await; + shutdown_sender.send(()).await; +} + +async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnectionHandlers) { + use futures::future::FutureExt; + + info!(ctx, "starting smtp loop"); + let SmtpConnectionHandlers { + mut connection, + stop_receiver, + shutdown_sender, + idle_interrupt_receiver, + } = smtp_handlers; + + let ctx1 = ctx.clone(); + let fut = async move { + started.send(()).await; + let ctx = ctx1; + + let mut probe_network = false; + loop { + match job::load_next(&ctx, Thread::Smtp, probe_network).await { + Some(job) => { + info!(ctx, "executing smtp job"); + job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await; + probe_network = false; + } + None => { + // Fake Idle + info!(ctx, "smtp fake idle - started"); + probe_network = idle_interrupt_receiver.recv().await.unwrap_or_default(); + info!(ctx, "smtp fake idle - interrupted") + } + } + } + }; + + stop_receiver + .recv() + .map(|_| { + info!(ctx, "shutting down smtp loop"); + }) + .race(fut) + .await; + shutdown_sender.send(()).await; +} + +impl Scheduler { + /// Start the scheduler, panics if it is already running. + pub async fn start(&mut self, ctx: Context) { + let (mvbox, mvbox_handlers) = ImapConnectionState::new(); + let (sentbox, sentbox_handlers) = ImapConnectionState::new(); + let (smtp, smtp_handlers) = SmtpConnectionState::new(); + let (inbox, inbox_handlers) = ImapConnectionState::new(); + + *self = Scheduler::Running { + inbox, + mvbox, + sentbox, + smtp, + inbox_handle: None, + mvbox_handle: None, + sentbox_handle: None, + smtp_handle: None, + }; + + let (inbox_start_send, inbox_start_recv) = channel(1); + if let Scheduler::Running { inbox_handle, .. } = self { + let ctx1 = ctx.clone(); + *inbox_handle = Some(task::spawn(async move { + inbox_loop(ctx1, inbox_start_send, inbox_handlers).await + })); + } + + let (mvbox_start_send, mvbox_start_recv) = channel(1); + if let Scheduler::Running { mvbox_handle, .. } = self { + let ctx1 = ctx.clone(); + *mvbox_handle = Some(task::spawn(async move { + simple_imap_loop( + ctx1, + mvbox_start_send, + mvbox_handlers, + "configured_mvbox_folder", + ) + .await + })); + } + + let (sentbox_start_send, sentbox_start_recv) = channel(1); + if let Scheduler::Running { sentbox_handle, .. } = self { + let ctx1 = ctx.clone(); + *sentbox_handle = Some(task::spawn(async move { + simple_imap_loop( + ctx1, + sentbox_start_send, + sentbox_handlers, + "configured_sentbox_folder", + ) + .await + })); + } + + let (smtp_start_send, smtp_start_recv) = channel(1); + if let Scheduler::Running { smtp_handle, .. } = self { + let ctx1 = ctx.clone(); + *smtp_handle = Some(task::spawn(async move { + smtp_loop(ctx1, smtp_start_send, smtp_handlers).await + })); + } + + // wait for all loops to be started + if let Err(err) = inbox_start_recv + .recv() + .try_join(mvbox_start_recv.recv()) + .try_join(sentbox_start_recv.recv()) + .try_join(smtp_start_recv.recv()) + .await + { + error!(ctx, "failed to start scheduler: {}", err); + } + + info!(ctx, "scheduler is running"); + } + + async fn maybe_network(&self) { + if !self.is_running() { + return; + } + + self.interrupt_inbox(true) + .join(self.interrupt_mvbox(true)) + .join(self.interrupt_sentbox(true)) + .join(self.interrupt_smtp(true)) + .await; + } + + async fn interrupt_inbox(&self, probe_network: bool) { + if let Scheduler::Running { ref inbox, .. } = self { + inbox.interrupt(probe_network).await; + } + } + + async fn interrupt_mvbox(&self, probe_network: bool) { + if let Scheduler::Running { ref mvbox, .. } = self { + mvbox.interrupt(probe_network).await; + } + } + + async fn interrupt_sentbox(&self, probe_network: bool) { + if let Scheduler::Running { ref sentbox, .. } = self { + sentbox.interrupt(probe_network).await; + } + } + + async fn interrupt_smtp(&self, probe_network: bool) { + if let Scheduler::Running { ref smtp, .. } = self { + smtp.interrupt(probe_network).await; + } + } + + /// Halts the scheduler, must be called first, and then `stop`. + pub(crate) async fn pre_stop(&self) -> StopToken { + match self { + Scheduler::Stopped => { + panic!("WARN: already stopped"); + } + Scheduler::Running { + inbox, + mvbox, + sentbox, + smtp, + .. + } => { + inbox + .stop() + .join(mvbox.stop()) + .join(sentbox.stop()) + .join(smtp.stop()) + .await; + + StopToken + } + } + } + + /// Halt the scheduler, must only be called after pre_stop. + pub(crate) async fn stop(&mut self, _t: StopToken) { + match self { + Scheduler::Stopped => { + panic!("WARN: already stopped"); + } + Scheduler::Running { + inbox_handle, + mvbox_handle, + sentbox_handle, + smtp_handle, + .. + } => { + inbox_handle.take().expect("inbox not started").await; + mvbox_handle.take().expect("mvbox not started").await; + sentbox_handle.take().expect("sentbox not started").await; + smtp_handle.take().expect("smtp not started").await; + + *self = Scheduler::Stopped; + } + } + } + + /// Check if the scheduler is running. + pub fn is_running(&self) -> bool { + match self { + Scheduler::Running { .. } => true, + _ => false, + } + } +} + +/// Connection state logic shared between imap and smtp connections. +#[derive(Debug)] +struct ConnectionState { + /// Channel to notify that shutdown has completed. + shutdown_receiver: Receiver<()>, + /// Channel to interrupt the whole connection. + stop_sender: Sender<()>, + /// Channel to interrupt idle. + idle_interrupt_sender: Sender, +} + +impl ConnectionState { + /// Shutdown this connection completely. + async fn stop(&self) { + // Trigger shutdown of the run loop. + self.stop_sender.send(()).await; + // Wait for a notification that the run loop has been shutdown. + self.shutdown_receiver.recv().await.ok(); + } + + async fn interrupt(&self, probe_network: bool) { + // Use try_send to avoid blocking on interrupts. + self.idle_interrupt_sender.try_send(probe_network).ok(); + } +} + +#[derive(Debug)] +pub(crate) struct SmtpConnectionState { + state: ConnectionState, +} + +impl SmtpConnectionState { + fn new() -> (Self, SmtpConnectionHandlers) { + let (stop_sender, stop_receiver) = channel(1); + let (shutdown_sender, shutdown_receiver) = channel(1); + let (idle_interrupt_sender, idle_interrupt_receiver) = channel(1); + + let handlers = SmtpConnectionHandlers { + connection: Smtp::new(), + stop_receiver, + shutdown_sender, + idle_interrupt_receiver, + }; + + let state = ConnectionState { + idle_interrupt_sender, + shutdown_receiver, + stop_sender, + }; + + let conn = SmtpConnectionState { state }; + + (conn, handlers) + } + + /// Interrupt any form of idle. + async fn interrupt(&self, probe_network: bool) { + self.state.interrupt(probe_network).await; + } + + /// Shutdown this connection completely. + async fn stop(&self) { + self.state.stop().await; + } +} + +#[derive(Debug)] +struct SmtpConnectionHandlers { + connection: Smtp, + stop_receiver: Receiver<()>, + shutdown_sender: Sender<()>, + idle_interrupt_receiver: Receiver, +} + +#[derive(Debug)] +pub(crate) struct ImapConnectionState { + state: ConnectionState, +} + +impl ImapConnectionState { + /// Construct a new connection. + fn new() -> (Self, ImapConnectionHandlers) { + let (stop_sender, stop_receiver) = channel(1); + let (shutdown_sender, shutdown_receiver) = channel(1); + let (idle_interrupt_sender, idle_interrupt_receiver) = channel(1); + + let handlers = ImapConnectionHandlers { + connection: Imap::new(idle_interrupt_receiver), + stop_receiver, + shutdown_sender, + }; + + let state = ConnectionState { + idle_interrupt_sender, + shutdown_receiver, + stop_sender, + }; + + let conn = ImapConnectionState { state }; + + (conn, handlers) + } + + /// Interrupt any form of idle. + async fn interrupt(&self, probe_network: bool) { + self.state.interrupt(probe_network).await; + } + + /// Shutdown this connection completely. + async fn stop(&self) { + self.state.stop().await; + } +} + +#[derive(Debug)] +struct ImapConnectionHandlers { + connection: Imap, + stop_receiver: Receiver<()>, + shutdown_sender: Sender<()>, +} + +async fn get_watch_folder(context: &Context, config_name: impl AsRef) -> Option { + match context + .sql + .get_raw_config(context, config_name.as_ref()) + .await + { + Some(name) => Some(name), + None => { + if config_name.as_ref() == "configured_inbox_folder" { + // initialized with old version, so has not set configured_inbox_folder + Some("INBOX".to_string()) + } else { + None + } + } + } +} diff --git a/src/securejoin.rs b/src/securejoin.rs index 62753fe78..580238881 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,5 +1,7 @@ //! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol) +use std::time::Duration; + use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; @@ -30,7 +32,7 @@ macro_rules! joiner_progress { $progress >= 0 && $progress <= 1000, "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" ); - $context.call_cb($crate::events::Event::SecurejoinJoinerProgress { + $context.emit_event($crate::events::Event::SecurejoinJoinerProgress { contact_id: $contact_id, progress: $progress, }); @@ -43,7 +45,7 @@ macro_rules! inviter_progress { $progress >= 0 && $progress <= 1000, "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" ); - $context.call_cb($crate::events::Event::SecurejoinInviterProgress { + $context.emit_event($crate::events::Event::SecurejoinInviterProgress { contact_id: $contact_id, progress: $progress, }); @@ -55,7 +57,7 @@ macro_rules! get_qr_attr { $context .bob .read() - .unwrap() + .await .qr_scan .as_ref() .unwrap() @@ -65,7 +67,7 @@ macro_rules! get_qr_attr { }; } -pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option { +pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option { /*======================================================= ==== Alice - the inviter side ==== ==== Step 1 in "Setup verified contact" protocol ==== @@ -73,13 +75,14 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option< let fingerprint: String; - ensure_secret_key_exists(context).ok(); + ensure_secret_key_exists(context).await.ok(); // invitenumber will be used to allow starting the handshake, // auth will be used to verify the fingerprint - let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id); - let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id); - let self_addr = match context.get_config(Config::ConfiguredAddr) { + let invitenumber = + token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id).await; + let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id).await; + let self_addr = match context.get_config(Config::ConfiguredAddr).await { Some(addr) => addr, None => { error!(context, "Not configured, cannot generate QR code.",); @@ -87,9 +90,12 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option< } }; - let self_name = context.get_config(Config::Displayname).unwrap_or_default(); + let self_name = context + .get_config(Config::Displayname) + .await + .unwrap_or_default(); - fingerprint = match get_self_fingerprint(context) { + fingerprint = match get_self_fingerprint(context).await { Some(fp) => fp, None => { return None; @@ -103,7 +109,7 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option< let qr = if !group_chat_id.is_unset() { // parameters used: a=g=x=i=s= - if let Ok(chat) = Chat::load_from_db(context, group_chat_id) { + if let Ok(chat) = Chat::load_from_db(context, group_chat_id).await { let group_name = chat.get_name(); let group_name_urlencoded = utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string(); @@ -134,8 +140,8 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option< qr } -fn get_self_fingerprint(context: &Context) -> Option { - match SignedPublicKey::load_self(context) { +async fn get_self_fingerprint(context: &Context) -> Option { + match SignedPublicKey::load_self(context).await { Ok(key) => Some(Key::from(key).fingerprint()), Err(_) => { warn!(context, "get_self_fingerprint(): failed to load key"); @@ -144,35 +150,48 @@ fn get_self_fingerprint(context: &Context) -> Option { } } +async fn cleanup( + context: &Context, + contact_chat_id: ChatId, + ongoing_allocated: bool, + join_vg: bool, +) -> ChatId { + let mut bob = context.bob.write().await; + bob.expects = 0; + let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS { + if join_vg { + chat::get_chat_id_by_grpid( + context, + bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(), + ) + .await + .unwrap_or((ChatId::new(0), false, Blocked::Not)) + .0 + } else { + contact_chat_id + } + } else { + ChatId::new(0) + }; + bob.qr_scan = None; + + if ongoing_allocated { + context.free_ongoing().await; + } + ret_chat_id +} + /// Take a scanned QR-code and do the setup-contact/join-group handshake. /// See the ffi-documentation for more details. -pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { - let cleanup = - |context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| { - let mut bob = context.bob.write().unwrap(); - bob.expects = 0; - let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS { - if join_vg { - chat::get_chat_id_by_grpid( - context, - bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(), - ) - .unwrap_or((ChatId::new(0), false, Blocked::Not)) - .0 - } else { - contact_chat_id - } - } else { - ChatId::new(0) - }; - bob.qr_scan = None; +pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { + if context.alloc_ongoing().await.is_err() { + return cleanup(&context, ChatId::new(0), false, false).await; + } - if ongoing_allocated { - context.free_ongoing(); - } - ret_chat_id - }; + securejoin(context, qr).await +} +async fn securejoin(context: &Context, qr: &str) -> ChatId { /*======================================================== ==== Bob - the joiner's side ===== ==== Step 2 in "Setup verified contact" protocol ===== @@ -182,29 +201,26 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { let mut join_vg: bool = false; info!(context, "Requesting secure-join ...",); - ensure_secret_key_exists(context).ok(); - if !context.alloc_ongoing() { - return cleanup(&context, contact_chat_id, false, join_vg); - } - let qr_scan = check_qr(context, &qr); + ensure_secret_key_exists(context).await.ok(); + let qr_scan = check_qr(context, &qr).await; if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup { error!(context, "Unknown QR code.",); - return cleanup(&context, contact_chat_id, true, join_vg); + return cleanup(&context, contact_chat_id, true, join_vg).await; } - contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id) { + contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await { Ok(chat_id) => chat_id, Err(_) => { error!(context, "Unknown contact."); - return cleanup(&context, contact_chat_id, true, join_vg); + return cleanup(&context, contact_chat_id, true, join_vg).await; } }; - if context.shall_stop_ongoing() { - return cleanup(&context, contact_chat_id, true, join_vg); + if context.shall_stop_ongoing().await { + return cleanup(&context, contact_chat_id, true, join_vg).await; } join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup; { - let mut bob = context.bob.write().unwrap(); + let mut bob = context.bob.write().await; bob.status = 0; bob.qr_scan = Some(qr_scan); } @@ -213,7 +229,7 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { context .bob .read() - .unwrap() + .await .qr_scan .as_ref() .unwrap() @@ -221,16 +237,22 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { .as_ref() .unwrap(), contact_chat_id, - ) { + ) + .await + { // the scanned fingerprint matches Alice's key, // we can proceed to step 4b) directly and save two mails info!(context, "Taking protocol shortcut."); - context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM; - joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400); - let own_fingerprint = get_self_fingerprint(context).unwrap_or_default(); + context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM; + joiner_progress!( + context, + chat_id_2_contact_id(context, contact_chat_id).await, + 400 + ); + let own_fingerprint = get_self_fingerprint(context).await.unwrap_or_default(); // Bob -> Alice - send_handshake_msg( + if let Err(err) = send_handshake_msg( context, contact_chat_id, if join_vg { @@ -245,43 +267,53 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { } else { "".to_string() }, - ); + ) + .await + { + error!(context, "failed to send handshake message: {}", err); + return cleanup(&context, contact_chat_id, true, join_vg).await; + } } else { - context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED; + context.bob.write().await.expects = DC_VC_AUTH_REQUIRED; // Bob -> Alice - send_handshake_msg( + if let Err(err) = send_handshake_msg( context, contact_chat_id, if join_vg { "vg-request" } else { "vc-request" }, get_qr_attr!(context, invitenumber), None, "", - ); + ) + .await + { + error!(context, "failed to send handshake message: {}", err); + return cleanup(&context, contact_chat_id, true, join_vg).await; + } } if join_vg { // for a group-join, wait until the secure-join is done and the group is created - while !context.shall_stop_ongoing() { - std::thread::sleep(std::time::Duration::from_millis(200)); + while !context.shall_stop_ongoing().await { + async_std::task::sleep(Duration::from_millis(50)).await; } - cleanup(&context, contact_chat_id, true, join_vg) + cleanup(&context, contact_chat_id, true, join_vg).await } else { // for a one-to-one-chat, the chat is already known, return the chat-id, // the verification runs in background - context.free_ongoing(); + context.free_ongoing().await; contact_chat_id } } -fn send_handshake_msg( +async fn send_handshake_msg( context: &Context, contact_chat_id: ChatId, step: &str, param2: impl AsRef, fingerprint: Option, grpid: impl AsRef, -) { +) -> Result<(), HandshakeError> { let mut msg = Message::default(); msg.viewtype = Viewtype::Text; msg.text = Some(format!("Secure-Join: {}", step)); @@ -309,12 +341,16 @@ fn send_handshake_msg( } else { msg.param.set_int(Param::GuaranteeE2ee, 1); } - // TODO. handle cleanup on error - chat::send_msg(context, contact_chat_id, &mut msg).unwrap_or_default(); + + chat::send_msg(context, contact_chat_id, &mut msg) + .await + .map_err(HandshakeError::MsgSendFailed)?; + + Ok(()) } -fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 { - let contacts = chat::get_chat_contacts(context, contact_chat_id); +async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 { + let contacts = chat::get_chat_contacts(context, contact_chat_id).await; if contacts.len() == 1 { contacts[0] } else { @@ -322,17 +358,16 @@ fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 { } } -fn fingerprint_equals_sender( +async fn fingerprint_equals_sender( context: &Context, fingerprint: impl AsRef, contact_chat_id: ChatId, ) -> bool { - let contacts = chat::get_chat_contacts(context, contact_chat_id); + let contacts = chat::get_chat_contacts(context, contact_chat_id).await; if contacts.len() == 1 { - if let Ok(contact) = Contact::load_from_db(context, contacts[0]) { - if let Some(peerstate) = Peerstate::from_addr(context, &context.sql, contact.get_addr()) - { + if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await { + if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await { let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref()); if peerstate.public_key_fingerprint.is_some() && &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap() @@ -360,6 +395,8 @@ pub(crate) enum HandshakeError { ChatNotFound { group: String }, #[error("No configured self address found")] NoSelfAddr, + #[error("Failed to send message")] + MsgSendFailed(#[source] Error), } /// What to do with a Secure-Join handshake message after it was handled. @@ -386,7 +423,7 @@ pub(crate) enum HandshakeMessage { /// When handle_securejoin_handshake() is called, /// the message is not yet filed in the database; /// this is done by receive_imf() later on as needed. -pub(crate) fn handle_securejoin_handshake( +pub(crate) async fn handle_securejoin_handshake( context: &Context, mime_message: &MimeMessage, contact_id: u32, @@ -404,10 +441,10 @@ pub(crate) fn handle_securejoin_handshake( ); let contact_chat_id = - match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) { + match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).await { Ok((chat_id, blocked)) => { if blocked != Blocked::Not { - chat_id.unblock(context); + chat_id.unblock(context).await; } chat_id } @@ -439,7 +476,7 @@ pub(crate) fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } }; - if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) { + if !token::exists(context, token::Namespace::InviteNumber, &invitenumber).await { warn!(context, "Secure-join denied (bad invitenumber)."); return Ok(HandshakeMessage::Ignore); } @@ -455,7 +492,8 @@ pub(crate) fn handle_securejoin_handshake( "", None, "", - ); + ) + .await?; Ok(HandshakeMessage::Done) } "vg-auth-required" | "vc-auth-required" => { @@ -466,7 +504,7 @@ pub(crate) fn handle_securejoin_handshake( // verify that Alice's Autocrypt key and fingerprint matches the QR-code let cond = { - let bob = context.bob.read().unwrap(); + let bob = context.bob.read().await; let scan = bob.qr_scan.as_ref(); scan.is_none() || bob.expects != DC_VC_AUTH_REQUIRED @@ -490,25 +528,29 @@ pub(crate) fn handle_securejoin_handshake( } else { "Not encrypted." }, - ); - context.bob.write().unwrap().status = 0; // secure-join failed - context.stop_ongoing(); + ) + .await; + context.bob.write().await.status = 0; // secure-join failed + context.stop_ongoing().await; return Ok(HandshakeMessage::Ignore); } - if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) { + if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) + .await + { could_not_establish_secure_connection( context, contact_chat_id, "Fingerprint mismatch on joiner-side.", - ); - context.bob.write().unwrap().status = 0; // secure-join failed - context.stop_ongoing(); + ) + .await; + context.bob.write().await.status = 0; // secure-join failed + context.stop_ongoing().await; return Ok(HandshakeMessage::Ignore); } info!(context, "Fingerprint verified.",); - let own_fingerprint = get_self_fingerprint(context).unwrap(); + let own_fingerprint = get_self_fingerprint(context).await.unwrap(); joiner_progress!(context, contact_id, 400); - context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM; + context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM; // Bob -> Alice send_handshake_msg( @@ -522,7 +564,8 @@ pub(crate) fn handle_securejoin_handshake( } else { "".to_string() }, - ); + ) + .await?; Ok(HandshakeMessage::Done) } "vg-request-with-auth" | "vc-request-with-auth" => { @@ -540,7 +583,8 @@ pub(crate) fn handle_securejoin_handshake( context, contact_chat_id, "Fingerprint not provided.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } }; @@ -549,15 +593,17 @@ pub(crate) fn handle_securejoin_handshake( context, contact_chat_id, "Auth not encrypted.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } - if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) { + 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; return Ok(HandshakeMessage::Ignore); } info!(context, "Fingerprint verified.",); @@ -569,25 +615,28 @@ pub(crate) fn handle_securejoin_handshake( context, contact_chat_id, "Auth not provided.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } }; - if !token::exists(context, token::Namespace::Auth, &auth_0) { - could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid."); + if !token::exists(context, token::Namespace::Auth, &auth_0).await { + could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.") + .await; return Ok(HandshakeMessage::Ignore); } - if mark_peer_as_verified(context, fingerprint).is_err() { + if mark_peer_as_verified(context, fingerprint).await.is_err() { could_not_establish_secure_connection( context, contact_chat_id, "Fingerprint mismatch on inviter-side.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } - Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited); + Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await; info!(context, "Auth verified.",); - secure_connection_established(context, contact_chat_id); + secure_connection_established(context, contact_chat_id).await; emit_event!(context, Event::ContactsChanged(Some(contact_id))); inviter_progress!(context, contact_id, 600); if join_vg { @@ -601,10 +650,11 @@ pub(crate) fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } }; - match chat::get_chat_id_by_grpid(context, field_grpid) { + match chat::get_chat_id_by_grpid(context, field_grpid).await { Ok((group_chat_id, _, _)) => { if let Err(err) = chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true) + .await { error!(context, "failed to add contact: {}", err); } @@ -625,7 +675,9 @@ pub(crate) fn handle_securejoin_handshake( "", Some(fingerprint.clone()), "", - ); + ) + .await?; + inviter_progress!(context, contact_id, 1000); } Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed) @@ -641,12 +693,12 @@ pub(crate) fn handle_securejoin_handshake( HandshakeMessage::Ignore }; - if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM { + if context.bob.read().await.expects != DC_VC_CONTACT_CONFIRM { info!(context, "Message belongs to a different handshake.",); return Ok(abort_retval); } let cond = { - let bob = context.bob.read().unwrap(); + let bob = context.bob.read().await; let scan = bob.qr_scan.as_ref(); scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup) }; @@ -667,6 +719,7 @@ pub(crate) fn handle_securejoin_handshake( // only after we have returned. It does not impact // the security invariants of secure-join however. let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id) + .await .unwrap_or((ChatId::new(0), false, Blocked::Not)); // when joining a non-verified group // the vg-member-added message may be unencrypted @@ -684,20 +737,25 @@ pub(crate) fn handle_securejoin_handshake( context, contact_chat_id, "Contact confirm message not encrypted.", - ); - context.bob.write().unwrap().status = 0; + ) + .await; + context.bob.write().await.status = 0; return Ok(abort_retval); } - if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() { + if mark_peer_as_verified(context, &scanned_fingerprint_of_alice) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_chat_id, "Fingerprint mismatch on joiner-side.", - ); + ) + .await; return Ok(abort_retval); } - Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined); + Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await; emit_event!(context, Event::ContactsChanged(None)); let cg_member_added = mime_message .get(HeaderDef::ChatGroupMemberAdded) @@ -706,13 +764,14 @@ pub(crate) fn handle_securejoin_handshake( if join_vg && !context .is_self_addr(cg_member_added) + .await .map_err(|_| HandshakeError::NoSelfAddr)? { info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group)."); return Ok(abort_retval); } - secure_connection_established(context, contact_chat_id); - context.bob.write().unwrap().expects = 0; + secure_connection_established(context, contact_chat_id).await; + context.bob.write().await.expects = 0; // Bob -> Alice send_handshake_msg( @@ -726,10 +785,11 @@ pub(crate) fn handle_securejoin_handshake( "", Some(scanned_fingerprint_of_alice), "", - ); + ) + .await?; - context.bob.write().unwrap().status = 1; - context.stop_ongoing(); + context.bob.write().await.status = 1; + context.stop_ongoing().await; Ok(if join_vg { HandshakeMessage::Propagate } else { @@ -742,8 +802,8 @@ pub(crate) fn handle_securejoin_handshake( ==== Step 8 in "Out-of-band verified groups" protocol ==== ==========================================================*/ - if let Ok(contact) = Contact::get_by_id(context, contact_id) { - if contact.is_verified(context) == VerifiedStatus::Unverified { + if let Ok(contact) = Contact::get_by_id(context, contact_id).await { + if contact.is_verified(context).await == VerifiedStatus::Unverified { warn!(context, "{} invalid.", step); return Ok(HandshakeMessage::Ignore); } @@ -754,7 +814,7 @@ pub(crate) fn handle_securejoin_handshake( .get(HeaderDef::SecureJoinGroup) .map(|s| s.as_str()) .unwrap_or_else(|| ""); - if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid) { + if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await { warn!(context, "Failed to lookup chat_id from grpid: {}", err); return Err(HandshakeError::ChatNotFound { group: field_grpid.to_string(), @@ -791,7 +851,7 @@ pub(crate) fn handle_securejoin_handshake( /// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm /// before sending vg-member-added-received - so, if we observe vg-member-added-received, /// we can mark the peer as verified as well. -pub(crate) fn observe_securejoin_on_other_device( +pub(crate) async fn observe_securejoin_on_other_device( context: &Context, mime_message: &MimeMessage, contact_id: u32, @@ -805,10 +865,10 @@ pub(crate) fn observe_securejoin_on_other_device( info!(context, "observing secure-join message \'{}\'", step); let contact_chat_id = - match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) { + match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).await { Ok((chat_id, blocked)) => { if blocked != Blocked::Not { - chat_id.unblock(context); + chat_id.unblock(context).await; } chat_id } @@ -828,13 +888,14 @@ pub(crate) fn observe_securejoin_on_other_device( if !encrypted_and_signed( context, mime_message, - get_self_fingerprint(context).unwrap_or_default(), + get_self_fingerprint(context).await.unwrap_or_default(), ) { could_not_establish_secure_connection( context, contact_chat_id, "Message not encrypted correctly.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) { @@ -844,16 +905,18 @@ pub(crate) fn observe_securejoin_on_other_device( context, contact_chat_id, "Fingerprint not provided, please update Delta Chat on all your devices.", - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } }; - if mark_peer_as_verified(context, fingerprint).is_err() { + if mark_peer_as_verified(context, fingerprint).await.is_err() { could_not_establish_secure_connection( context, contact_chat_id, format!("Fingerprint mismatch on observing {}.", step).as_ref(), - ); + ) + .await; return Ok(HandshakeMessage::Ignore); } Ok(if step.as_str() == "vg-member-added" { @@ -866,42 +929,50 @@ pub(crate) fn observe_securejoin_on_other_device( } } -fn secure_connection_established(context: &Context, contact_chat_id: ChatId) { - let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id); - let contact = Contact::get_by_id(context, contact_id); +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; + let contact = Contact::get_by_id(context, contact_id).await; + let addr = if let Ok(ref contact) = contact { contact.get_addr() } else { "?" }; - let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr); - chat::add_info_msg(context, contact_chat_id, msg); + let msg = context + .stock_string_repl_str(StockMessage::ContactVerified, addr) + .await; + chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, Event::ChatModified(contact_chat_id)); } -fn could_not_establish_secure_connection( +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); - let contact = Contact::get_by_id(context, contact_id); - let msg = context.stock_string_repl_str( - StockMessage::ContactNotVerified, - if let Ok(ref contact) = contact { - contact.get_addr() - } else { - "?" - }, - ); + 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 = context + .stock_string_repl_str( + StockMessage::ContactNotVerified, + if let Ok(ref contact) = contact { + contact.get_addr() + } else { + "?" + }, + ) + .await; - chat::add_info_msg(context, contact_chat_id, &msg); + chat::add_info_msg(context, contact_chat_id, &msg).await; error!(context, "{} ({})", &msg, details); } -fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef) -> Result<(), Error> { +async fn mark_peer_as_verified( + context: &Context, + fingerprint: impl AsRef, +) -> Result<(), Error> { if let Some(ref mut peerstate) = - Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref()) + Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref()).await { if peerstate.set_verified( PeerstateKeyType::PublicKey, @@ -912,6 +983,7 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef) -> Res peerstate.to_save = Some(ToSave::All); peerstate .save_to_db(&context.sql, false) + .await .unwrap_or_default(); return Ok(()); } @@ -955,18 +1027,25 @@ fn encrypted_and_signed( } } -pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<(), Error> { +pub async fn handle_degrade_event( + context: &Context, + peerstate: &Peerstate<'_>, +) -> Result<(), Error> { // - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal // - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes // together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother // with things they cannot fix, so the user is just kicked from the verified group // (and he will know this and can fix this) if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event { - let contact_id: i32 = match context.sql.query_get_value( - context, - "SELECT id FROM contacts WHERE addr=?;", - params![&peerstate.addr], - ) { + let contact_id: i32 = match context + .sql + .query_get_value( + context, + "SELECT id FROM contacts WHERE addr=?;", + paramsv![peerstate.addr], + ) + .await + { None => bail!( "contact with peerstate.addr {:?} not found", &peerstate.addr @@ -976,12 +1055,14 @@ pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result< if contact_id > 0 { let (contact_chat_id, _) = chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop) + .await .unwrap_or_default(); let msg = context - .stock_string_repl_str(StockMessage::ContactSetupChanged, peerstate.addr.clone()); + .stock_string_repl_str(StockMessage::ContactSetupChanged, peerstate.addr.clone()) + .await; - chat::add_info_msg(context, contact_chat_id, msg); + chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, Event::ChatModified(contact_chat_id)); } } diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index b8e698087..3a4188d6a 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -65,16 +65,16 @@ impl Smtp { } /// Disconnect the SMTP transport and drop it entirely. - pub fn disconnect(&mut self) { + pub async fn disconnect(&mut self) { if let Some(mut transport) = self.transport.take() { - async_std::task::block_on(transport.close()).ok(); + transport.close().await.ok(); } self.last_success = None; } /// Return true if smtp was connected but is not known to /// have been successfully used the last 60 seconds - pub fn has_maybe_stale_connection(&self) -> bool { + pub async fn has_maybe_stale_connection(&self) -> bool { if let Some(last_success) = self.last_success { Instant::now().duration_since(last_success).as_secs() > 60 } else { @@ -83,7 +83,7 @@ impl Smtp { } /// Check whether we are connected. - pub fn is_connected(&self) -> bool { + pub async fn is_connected(&self) -> bool { self.transport .as_ref() .map(|t| t.is_connected()) @@ -91,18 +91,14 @@ impl Smtp { } /// Connect using the provided login params. - pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> { - async_std::task::block_on(self.inner_connect(context, lp)) - } - - async fn inner_connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> { - if self.is_connected() { + pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> { + if self.is_connected().await { warn!(context, "SMTP already connected."); return Ok(()); } if lp.send_server.is_empty() || lp.send_port == 0 { - context.call_cb(Event::ErrorNetwork("SMTP bad parameters.".into())); + context.emit_event(Event::ErrorNetwork("SMTP bad parameters.".into())); return Err(Error::BadParameters); } @@ -111,6 +107,7 @@ impl Smtp { address: lp.addr.clone(), error: err, })?; + self.from = Some(from); let domain = &lp.send_server; @@ -123,7 +120,7 @@ impl Smtp { // oauth2 let addr = &lp.addr; let send_pw = &lp.send_pw; - let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false); + 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(), @@ -170,22 +167,23 @@ impl Smtp { .timeout(Some(Duration::from_secs(SMTP_TIMEOUT))); let mut trans = client.into_transport(); - - trans.connect().await.map_err(|err| { - let message = { - context.stock_string_repl_str2( + if let Err(err) = trans.connect().await { + let message = context + .stock_string_repl_str2( StockMessage::ServerResponse, format!("SMTP {}:{}", domain, port), err.to_string(), ) - }; + .await; + emit_event!(context, Event::ErrorNetwork(message)); - Error::ConnectionFailure(err) - })?; + return Err(Error::ConnectionFailure(err)); + } self.transport = Some(trans); self.last_success = Some(Instant::now()); - context.call_cb(Event::SmtpConnected(format!( + + context.emit_event(Event::SmtpConnected(format!( "SMTP-LOGIN as {} ok", lp.send_user, ))); diff --git a/src/smtp/send.rs b/src/smtp/send.rs index cade602f2..350d705e5 100644 --- a/src/smtp/send.rs +++ b/src/smtp/send.rs @@ -49,7 +49,7 @@ impl Smtp { if let Some(ref mut transport) = self.transport { transport.send(mail).await.map_err(Error::SendError)?; - context.call_cb(Event::SmtpMessageSent(format!( + context.emit_event(Event::SmtpMessageSent(format!( "Message len={} was smtp-sent to {}", message_len, recipients_display ))); diff --git a/src/sql.rs b/src/sql.rs index c5927aa04..c20e6bd89 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,11 +1,13 @@ //! # SQLite wrapper +use async_std::prelude::*; +use async_std::sync::RwLock; + use std::collections::HashSet; -use std::sync::{Arc, RwLock}; +use std::path::Path; use std::time::Duration; -use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS}; -use thread_local_object::ThreadLocal; +use rusqlite::{Connection, Error as SqlError, OpenFlags}; use crate::chat::{update_device_icon, update_saved_messages_icon}; use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH}; @@ -14,6 +16,16 @@ use crate::dc_tools::*; use crate::param::*; use crate::peerstate::*; +#[macro_export] +macro_rules! paramsv { + () => { + Vec::new() + }; + ($($param:expr),+ $(,)?) => { + vec![$(&$param as &dyn $crate::ToSql),+] + }; +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Sqlite Error: {0:?}")] @@ -40,15 +52,12 @@ pub type Result = std::result::Result; #[derive(DebugStub)] pub struct Sql { pool: RwLock>>, - #[debug_stub = "ThreadLocal"] - in_use: Arc>, } impl Default for Sql { fn default() -> Self { Self { pool: RwLock::new(None), - in_use: Arc::new(ThreadLocal::new()), } } } @@ -58,151 +67,157 @@ impl Sql { Self::default() } - pub fn is_open(&self) -> bool { - self.pool.read().unwrap().is_some() + pub async fn is_open(&self) -> bool { + self.pool.read().await.is_some() } - pub fn close(&self, context: &Context) { - let _ = self.pool.write().unwrap().take(); - self.in_use.remove(); + pub async fn close(&self) { + let _ = self.pool.write().await.take(); // drop closes the connection - - info!(context, "Database closed."); } // return true on success, false on failure - pub fn open(&self, context: &Context, dbfile: &std::path::Path, readonly: bool) -> bool { - match open(context, self, dbfile, readonly) { + pub async fn open>(&self, context: &Context, dbfile: T, readonly: bool) -> bool { + match open(context, self, dbfile, readonly).await { Ok(_) => true, - Err(Error::SqlAlreadyOpen) => false, - Err(_) => { - self.close(context); - false - } + Err(err) => match err.downcast_ref::() { + Some(Error::SqlAlreadyOpen) => false, + _ => { + self.close().await; + false + } + }, } } - pub fn execute

(&self, sql: &str, params: P) -> Result - where - P: IntoIterator, - P::Item: rusqlite::ToSql, - { - self.start_stmt(sql.to_string()); - self.with_conn(|conn| conn.execute(sql, params).map_err(Into::into)) - } - - pub fn with_conn(&self, g: G) -> Result - where - G: FnOnce(&mut Connection) -> Result, - { - let res = match &*self.pool.read().unwrap() { - Some(pool) => { - let mut conn = pool.get()?; - - // Only one process can make changes to the database at one time. - // busy_timeout defines, that if a second process wants write access, - // this second process will wait some milliseconds - // and try over until it gets write access or the given timeout is elapsed. - // If the second process does not get write access within the given timeout, - // sqlite3_step() will return the error SQLITE_BUSY. - // (without a busy_timeout, sqlite3_step() would return SQLITE_BUSY _at once_) - conn.busy_timeout(Duration::from_secs(10))?; - - g(&mut conn) - } - None => Err(Error::SqlNoConnection), + pub async fn execute>( + &self, + sql: S, + params: Vec<&dyn crate::ToSql>, + ) -> Result { + let res = { + let conn = self.get_conn().await?; + conn.execute(sql.as_ref(), params) }; - self.in_use.remove(); - res - } - pub fn prepare(&self, sql: &str, g: G) -> Result - where - G: FnOnce(Statement<'_>, &Connection) -> Result, - { - self.start_stmt(sql.to_string()); - self.with_conn(|conn| { - let stmt = conn.prepare(sql)?; - g(stmt, conn) - }) - } - - pub fn prepare2(&self, sql1: &str, sql2: &str, g: G) -> Result - where - G: FnOnce(Statement<'_>, Statement<'_>, &Connection) -> Result, - { - self.start_stmt(format!("{} - {}", sql1, sql2)); - self.with_conn(|conn| { - let stmt1 = conn.prepare(sql1)?; - let stmt2 = conn.prepare(sql2)?; - - g(stmt1, stmt2, conn) - }) + res.map_err(Into::into) } /// Prepares and executes the statement and maps a function over the resulting rows. /// Then executes the second function over the returned iterator and returns the /// result of that function. - pub fn query_map( + pub async fn query_map( &self, sql: impl AsRef, - params: P, + params: Vec<&dyn crate::ToSql>, f: F, mut g: G, ) -> Result where - P: IntoIterator, - P::Item: rusqlite::ToSql, F: FnMut(&rusqlite::Row) -> rusqlite::Result, G: FnMut(rusqlite::MappedRows) -> Result, { - self.start_stmt(sql.as_ref().to_string()); - self.with_conn(|conn| { - let mut stmt = conn.prepare(sql.as_ref())?; - let res = stmt.query_map(params, f)?; - g(res) - }) + let sql = sql.as_ref(); + + let conn = self.get_conn().await?; + let mut stmt = conn.prepare(sql)?; + let res = stmt.query_map(¶ms, f)?; + g(res) + } + + pub async fn get_conn( + &self, + ) -> Result> { + let lock = self.pool.read().await; + let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?; + let conn = pool.get()?; + + Ok(conn) + } + + pub async fn with_conn(&self, g: G) -> Result + where + H: Send + 'static, + G: Send + + 'static + + FnOnce(r2d2::PooledConnection) -> Result, + { + let lock = self.pool.read().await; + let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?; + let conn = pool.get()?; + + g(conn) + } + + pub async fn with_conn_async(&self, mut g: G) -> Result + where + G: FnMut(r2d2::PooledConnection) -> Fut, + Fut: Future> + Send, + { + let lock = self.pool.read().await; + let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?; + + let conn = pool.get()?; + g(conn).await } /// Return `true` if a query in the SQL statement it executes returns one or more /// rows and false if the SQL returns an empty set. - pub fn exists

(&self, sql: &str, params: P) -> Result - where - P: IntoIterator, - P::Item: rusqlite::ToSql, - { - self.start_stmt(sql.to_string()); - self.with_conn(|conn| { + pub async fn exists(&self, sql: &str, params: Vec<&dyn crate::ToSql>) -> Result { + let res = { + let conn = self.get_conn().await?; let mut stmt = conn.prepare(sql)?; - let res = stmt.exists(params)?; - Ok(res) - }) + stmt.exists(¶ms) + }; + + res.map_err(Into::into) } /// Execute a query which is expected to return one row. - pub fn query_row(&self, sql: impl AsRef, params: P, f: F) -> Result + pub async fn query_row( + &self, + sql: impl AsRef, + params: Vec<&dyn crate::ToSql>, + f: F, + ) -> Result where - P: IntoIterator, - P::Item: rusqlite::ToSql, F: FnOnce(&rusqlite::Row) -> rusqlite::Result, { - self.start_stmt(sql.as_ref().to_string()); - self.with_conn(|conn| conn.query_row(sql.as_ref(), params, f).map_err(Into::into)) + let sql = sql.as_ref(); + let res = { + let conn = self.get_conn().await?; + conn.query_row(sql, params, f) + }; + + res.map_err(Into::into) + } + + pub async fn table_exists(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_string(); + self.with_conn(move |conn| { + let mut exists = false; + conn.pragma(None, "table_info", &name, |_row| { + // will only be executed if the info was found + exists = true; + Ok(()) + })?; + + Ok(exists) + }) + .await } /// Execute a query which is expected to return zero or one row. - pub fn query_row_optional( + pub async fn query_row_optional( &self, sql: impl AsRef, - params: P, + params: Vec<&dyn crate::ToSql>, f: F, ) -> Result> where - P: IntoIterator, - P::Item: rusqlite::ToSql, F: FnOnce(&rusqlite::Row) -> rusqlite::Result, { - match self.query_row(sql, params, f) { + match self.query_row(sql, params, f).await { Ok(res) => Ok(Some(res)), Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None), Err(Error::Sql(rusqlite::Error::InvalidColumnType( @@ -214,32 +229,33 @@ impl Sql { } } - pub fn table_exists(&self, name: impl AsRef) -> bool { - self.with_conn(|conn| table_exists(conn, name)) - .unwrap_or_default() - } - /// 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 fn query_get_value_result(&self, query: &str, params: P) -> Result> + pub async fn query_get_value_result( + &self, + query: &str, + params: Vec<&dyn crate::ToSql>, + ) -> Result> where - P: IntoIterator, - P::Item: rusqlite::ToSql, T: rusqlite::types::FromSql, { self.query_row_optional(query, params, |row| row.get::<_, T>(0)) + .await } /// Not resultified version of `query_get_value_result`. Returns /// `None` on error. - pub fn query_get_value(&self, context: &Context, query: &str, params: P) -> Option + pub async fn query_get_value( + &self, + context: &Context, + query: &str, + params: Vec<&dyn crate::ToSql>, + ) -> Option where - P: IntoIterator, - P::Item: rusqlite::ToSql, T: rusqlite::types::FromSql, { - match self.query_get_value_result(query, params) { + match self.query_get_value_result(query, params).await { Ok(res) => res, Err(err) => { warn!(context, "sql: Failed query_row: {}", err); @@ -252,42 +268,38 @@ impl Sql { /// /// Setting `None` deletes the value. On failure an error message /// will already have been logged. - pub fn set_raw_config( + pub async fn set_raw_config( &self, context: &Context, key: impl AsRef, value: Option<&str>, ) -> Result<()> { - if !self.is_open() { + if !self.is_open().await { error!(context, "set_raw_config(): Database not ready."); return Err(Error::SqlNoConnection); } let key = key.as_ref(); let res = if let Some(ref value) = value { - let exists = self.exists("SELECT value FROM config WHERE keyname=?;", params![key])?; + let exists = self + .exists("SELECT value FROM config WHERE keyname=?;", paramsv![key]) + .await?; if exists { - execute( - context, - self, + self.execute( "UPDATE config SET value=? WHERE keyname=?;", - params![value, key], + paramsv![(*value).to_string(), key.to_string()], ) + .await } else { - execute( - context, - self, + self.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", - params![key, value], + paramsv![key.to_string(), (*value).to_string()], ) + .await } } else { - execute( - context, - self, - "DELETE FROM config WHERE keyname=?;", - params![key], - ) + self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key]) + .await }; match res { @@ -300,696 +312,111 @@ impl Sql { } /// Get configuration options from the database. - pub fn get_raw_config(&self, context: &Context, key: impl AsRef) -> Option { - if !self.is_open() || key.as_ref().is_empty() { + pub async fn get_raw_config(&self, context: &Context, key: impl AsRef) -> Option { + if !self.is_open().await || key.as_ref().is_empty() { return None; } self.query_get_value( context, "SELECT value FROM config WHERE keyname=?;", - params![key.as_ref()], + paramsv![key.as_ref().to_string()], ) + .await } - pub fn set_raw_config_int( + pub async fn set_raw_config_int( &self, context: &Context, key: impl AsRef, value: i32, ) -> Result<()> { self.set_raw_config(context, key, Some(&format!("{}", value))) + .await } - pub fn get_raw_config_int(&self, context: &Context, key: impl AsRef) -> Option { + pub async fn get_raw_config_int(&self, context: &Context, key: impl AsRef) -> Option { self.get_raw_config(context, key) + .await .and_then(|s| s.parse().ok()) } - pub fn get_raw_config_bool(&self, context: &Context, key: impl AsRef) -> bool { + pub async fn get_raw_config_bool(&self, context: &Context, key: impl AsRef) -> bool { // Not the most obvious way to encode bool as string, but it is matter // of backward compatibility. - self.get_raw_config_int(context, key).unwrap_or_default() > 0 + let res = self.get_raw_config_int(context, key).await; + res.unwrap_or_default() > 0 } - pub fn set_raw_config_bool(&self, context: &Context, key: T, value: bool) -> Result<()> + pub async fn set_raw_config_bool(&self, context: &Context, key: T, value: bool) -> Result<()> where T: AsRef, { let value = if value { Some("1") } else { None }; - self.set_raw_config(context, key, value) + self.set_raw_config(context, key, value).await } - pub fn set_raw_config_int64( + pub async fn set_raw_config_int64( &self, context: &Context, key: impl AsRef, value: i64, ) -> Result<()> { self.set_raw_config(context, key, Some(&format!("{}", value))) + .await } - pub fn get_raw_config_int64(&self, context: &Context, key: impl AsRef) -> Option { + pub async fn get_raw_config_int64( + &self, + context: &Context, + key: impl AsRef, + ) -> Option { self.get_raw_config(context, key) + .await .and_then(|r| r.parse().ok()) } - pub fn start_stmt(&self, stmt: impl AsRef) { - if let Some(query) = self.in_use.get_cloned() { - let bt = backtrace::Backtrace::new(); - eprintln!("old query: {}", query); - eprintln!("Connection is already used from this thread: {:?}", bt); - panic!("Connection is already used from this thread"); - } + /// 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, + _context: &Context, + table: impl AsRef, + field: impl AsRef, + value: impl AsRef, + ) -> Result { + let res = { + let mut conn = self.get_conn().await?; + get_rowid(&mut conn, table, field, value) + }; - self.in_use.set(stmt.as_ref().to_string()); - } -} - -fn table_exists(conn: &Connection, name: impl AsRef) -> Result { - let mut exists = false; - conn.pragma(None, "table_info", &name.as_ref().to_string(), |_row| { - // will only be executed if the info was found - exists = true; - Ok(()) - })?; - Ok(exists) -} - -#[allow(clippy::cognitive_complexity)] -fn open( - context: &Context, - sql: &Sql, - dbfile: impl AsRef, - readonly: bool, -) -> Result<()> { - if sql.is_open() { - error!( - context, - "Cannot open, database \"{:?}\" already opened.", - dbfile.as_ref(), - ); - return Err(Error::SqlAlreadyOpen); + res.map_err(Into::into) } - let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX; - if readonly { - open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY); - } else { - open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); - open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE); - } + pub async fn get_rowid2( + &self, + _context: &Context, + table: impl AsRef, + field: impl AsRef, + value: i64, + field2: impl AsRef, + value2: i32, + ) -> Result { + let res = { + let mut conn = self.get_conn().await?; + get_rowid2(&mut conn, table, field, value, field2, value2) + }; - // this actually creates min_idle database handles just now. - // therefore, with_init() must not try to modify the database as otherwise - // we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle) - let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile.as_ref()) - .with_flags(open_flags) - .with_init(|c| c.execute_batch("PRAGMA secure_delete=on;")); - let pool = r2d2::Pool::builder() - .min_idle(Some(2)) - .max_size(10) - .connection_timeout(std::time::Duration::new(60, 0)) - .build(mgr) - .map_err(Error::ConnectionPool)?; - - { - *sql.pool.write().unwrap() = Some(pool); - } - - if !readonly { - // journal_mode is persisted, it is sufficient to change it only for one handle. - // (nb: execute() always returns errors for this PRAGMA call, just discard it. - // but even if execute() would handle errors more gracefully, we should continue on errors - - // systems might not be able to handle WAL, in which case the standard-journal is used. - // that may be not optimal, but better than not working at all :) - sql.execute("PRAGMA journal_mode=WAL;", NO_PARAMS).ok(); - - let mut exists_before_update = false; - let mut dbversion_before_update = 0; - /* Init tables to dbversion=0 */ - if !sql.table_exists("config") { - info!( - context, - "First time init: creating tables in {:?}.", - dbfile.as_ref(), - ); - sql.execute( - "CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT);", - NO_PARAMS, - )?; - sql.execute("CREATE INDEX config_index1 ON config (keyname);", NO_PARAMS)?; - sql.execute( - "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 '');", - params![], - )?; - sql.execute( - "CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);", - params![], - )?; - sql.execute( - "CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);", - params![], - )?; - sql.execute( - "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);", - params![], - )?; - sql.execute( - "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 '');", - params![], - )?; - sql.execute("CREATE INDEX chats_index1 ON chats (grpid);", params![])?; - sql.execute( - "CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);", - params![], - )?; - sql.execute( - "CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);", - params![], - )?; - sql.execute( - "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');", - params![], - )?; - sql.execute( - "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 '');", - params![], - )?; - sql.execute("CREATE INDEX msgs_index1 ON msgs (rfc724_mid);", params![])?; - sql.execute("CREATE INDEX msgs_index2 ON msgs (chat_id);", params![])?; - sql.execute("CREATE INDEX msgs_index3 ON msgs (timestamp);", params![])?; - sql.execute("CREATE INDEX msgs_index4 ON msgs (state);", params![])?; - sql.execute( - "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');", - params![], - )?; - sql.execute( - "CREATE TABLE jobs (\ - id INTEGER PRIMARY KEY AUTOINCREMENT, \ - added_timestamp INTEGER, \ - desired_timestamp INTEGER DEFAULT 0, \ - action INTEGER, \ - foreign_id INTEGER, \ - param TEXT DEFAULT '');", - params![], - )?; - sql.execute( - "CREATE INDEX jobs_index1 ON jobs (desired_timestamp);", - params![], - )?; - if !sql.table_exists("config") - || !sql.table_exists("contacts") - || !sql.table_exists("chats") - || !sql.table_exists("chats_contacts") - || !sql.table_exists("msgs") - || !sql.table_exists("jobs") - { - error!( - context, - "Cannot create tables in new database \"{:?}\".", - dbfile.as_ref(), - ); - // cannot create the tables - maybe we cannot write? - return Err(Error::SqlFailedToOpen); - } else { - sql.set_raw_config_int(context, "dbversion", 0)?; - } - } else { - exists_before_update = true; - dbversion_before_update = sql - .get_raw_config_int(context, "dbversion") - .unwrap_or_default(); - } - - // (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 mut dbversion = dbversion_before_update; - let mut recalc_fingerprints = false; - let mut update_icons = false; - - if dbversion < 1 { - info!(context, "[migration] v1"); - sql.execute( - "CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');", - params![], - )?; - sql.execute( - "CREATE INDEX leftgrps_index1 ON leftgrps (grpid);", - params![], - )?; - dbversion = 1; - sql.set_raw_config_int(context, "dbversion", 1)?; - } - if dbversion < 2 { - info!(context, "[migration] v2"); - sql.execute( - "ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';", - params![], - )?; - dbversion = 2; - sql.set_raw_config_int(context, "dbversion", 2)?; - } - if dbversion < 7 { - info!(context, "[migration] v7"); - sql.execute( - "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);", - params![], - )?; - dbversion = 7; - sql.set_raw_config_int(context, "dbversion", 7)?; - } - if dbversion < 10 { - info!(context, "[migration] v10"); - sql.execute( - "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);", - params![], - )?; - sql.execute( - "CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);", - params![], - )?; - dbversion = 10; - sql.set_raw_config_int(context, "dbversion", 10)?; - } - if dbversion < 12 { - info!(context, "[migration] v12"); - sql.execute( - "CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);", - params![], - )?; - sql.execute( - "CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);", - params![], - )?; - dbversion = 12; - sql.set_raw_config_int(context, "dbversion", 12)?; - } - if dbversion < 17 { - info!(context, "[migration] v17"); - sql.execute( - "ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;", - params![], - )?; - sql.execute("CREATE INDEX chats_index2 ON chats (archived);", params![])?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;", - params![], - )?; - sql.execute("CREATE INDEX msgs_index5 ON msgs (starred);", params![])?; - dbversion = 17; - sql.set_raw_config_int(context, "dbversion", 17)?; - } - if dbversion < 18 { - info!(context, "[migration] v18"); - sql.execute( - "ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;", - params![], - )?; - sql.execute("ALTER TABLE acpeerstates ADD COLUMN gossip_key;", params![])?; - dbversion = 18; - sql.set_raw_config_int(context, "dbversion", 18)?; - } - 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("DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;", params![])?; - sql.execute( - "CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);", - params![], - )?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;", - params![], - )?; - dbversion = 27; - sql.set_raw_config_int(context, "dbversion", 27)?; - } - if dbversion < 34 { - info!(context, "[migration] v34"); - sql.execute( - "ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';", - params![], - )?; - sql.execute( - "ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';", - params![], - )?; - sql.execute( - "CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);", - params![], - )?; - sql.execute( - "CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);", - params![], - )?; - recalc_fingerprints = true; - dbversion = 34; - sql.set_raw_config_int(context, "dbversion", 34)?; - } - if dbversion < 39 { - info!(context, "[migration] v39"); - sql.execute( - "CREATE TABLE tokens ( id INTEGER PRIMARY KEY, namespc INTEGER DEFAULT 0, foreign_id INTEGER DEFAULT 0, token TEXT DEFAULT '', timestamp INTEGER DEFAULT 0);", - params![] - )?; - sql.execute( - "ALTER TABLE acpeerstates ADD COLUMN verified_key;", - params![], - )?; - sql.execute( - "ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';", - params![], - )?; - sql.execute( - "CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);", - params![], - )?; - dbversion = 39; - sql.set_raw_config_int(context, "dbversion", 39)?; - } - if dbversion < 40 { - info!(context, "[migration] v40"); - sql.execute( - "ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", - params![], - )?; - dbversion = 40; - sql.set_raw_config_int(context, "dbversion", 40)?; - } - if dbversion < 44 { - info!(context, "[migration] v44"); - sql.execute("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", params![])?; - dbversion = 44; - sql.set_raw_config_int(context, "dbversion", 44)?; - } - if dbversion < 46 { - info!(context, "[migration] v46"); - sql.execute( - "ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;", - params![], - )?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN mime_references TEXT;", - params![], - )?; - dbversion = 46; - sql.set_raw_config_int(context, "dbversion", 46)?; - } - if dbversion < 47 { - info!(context, "[migration] v47"); - sql.execute( - "ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", - params![], - )?; - dbversion = 47; - sql.set_raw_config_int(context, "dbversion", 47)?; - } - if dbversion < 48 { - info!(context, "[migration] v48"); - // NOTE: move_state is not used anymore - sql.execute( - "ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;", - params![], - )?; - - dbversion = 48; - sql.set_raw_config_int(context, "dbversion", 48)?; - } - if dbversion < 49 { - info!(context, "[migration] v49"); - sql.execute( - "ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;", - params![], - )?; - dbversion = 49; - sql.set_raw_config_int(context, "dbversion", 49)?; - } - 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(context, "show_emails", ShowEmails::All as i32)?; - } - dbversion = 50; - sql.set_raw_config_int(context, "dbversion", 50)?; - } - if dbversion < 53 { - info!(context, "[migration] v53"); - // the messages containing _only_ locations - // are also added to the database as _hidden_. - sql.execute( - "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);", - params![] - )?; - sql.execute( - "CREATE INDEX locations_index1 ON locations (from_id);", - params![], - )?; - sql.execute( - "CREATE INDEX locations_index2 ON locations (timestamp);", - params![], - )?; - sql.execute( - "ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;", - params![], - )?; - sql.execute( - "CREATE INDEX chats_index3 ON chats (locations_send_until);", - params![], - )?; - dbversion = 53; - sql.set_raw_config_int(context, "dbversion", 53)?; - } - if dbversion < 54 { - info!(context, "[migration] v54"); - sql.execute( - "ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;", - params![], - )?; - sql.execute("CREATE INDEX msgs_index6 ON msgs (location_id);", params![])?; - dbversion = 54; - sql.set_raw_config_int(context, "dbversion", 54)?; - } - if dbversion < 55 { - info!(context, "[migration] v55"); - sql.execute( - "ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;", - params![], - )?; - sql.set_raw_config_int(context, "dbversion", 55)?; - } - 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( - "CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);", - NO_PARAMS, - )?; - sql.execute( - "CREATE INDEX devmsglabels_index1 ON devmsglabels (label);", - NO_PARAMS, - )?; - if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() { - sql.set_raw_config_int(context, "bcc_self", 1)?; - } - sql.set_raw_config_int(context, "dbversion", 59)?; - } - if dbversion < 60 { - info!(context, "[migration] v60"); - sql.execute( - "ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;", - NO_PARAMS, - )?; - sql.set_raw_config_int(context, "dbversion", 60)?; - } - if dbversion < 61 { - info!(context, "[migration] v61"); - sql.execute( - "ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;", - NO_PARAMS, - )?; - update_icons = true; - sql.set_raw_config_int(context, "dbversion", 61)?; - } - if dbversion < 62 { - info!(context, "[migration] v62"); - sql.execute( - "ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;", - NO_PARAMS, - )?; - sql.set_raw_config_int(context, "dbversion", 62)?; - } - if dbversion < 63 { - info!(context, "[migration] v63"); - sql.execute("UPDATE chats SET grpid='' WHERE type=100", NO_PARAMS)?; - sql.set_raw_config_int(context, "dbversion", 63)?; - } - - // (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"); - sql.query_map( - "SELECT addr FROM acpeerstates;", - params![], - |row| row.get::<_, String>(0), - |addrs| { - for addr in addrs { - if let Some(ref mut peerstate) = Peerstate::from_addr(context, sql, &addr?) - { - peerstate.recalc_fingerprint(); - peerstate.save_to_db(sql, false)?; - } - } - Ok(()) - }, - )?; - } - if update_icons { - update_saved_messages_icon(context)?; - update_device_icon(context)?; - } - } - - info!(context, "Opened {:?}.", dbfile.as_ref(),); - - Ok(()) -} - -pub fn execute

(context: &Context, sql: &Sql, querystr: impl AsRef, params: P) -> Result<()> -where - P: IntoIterator, - P::Item: rusqlite::ToSql, -{ - match sql.execute(querystr.as_ref(), params) { - Ok(_) => Ok(()), - Err(err) => { - error!( - context, - "execute failed: {:?} for {}", - &err, - querystr.as_ref() - ); - Err(err) - } - } -} - -pub fn try_execute(context: &Context, sql: &Sql, querystr: impl AsRef) -> Result<()> { - // same as execute() but does not pass error to ui - match sql.execute(querystr.as_ref(), params![]) { - Ok(_) => Ok(()), - Err(err) => { - warn!( - context, - "Try-execute for \"{}\" failed: {}", - querystr.as_ref(), - &err, - ); - Err(err) - } + res.map_err(Into::into) } } pub fn get_rowid( - context: &Context, - sql: &Sql, + conn: &mut Connection, table: impl AsRef, field: impl AsRef, value: impl AsRef, -) -> u32 { - sql.start_stmt("get rowid".to_string()); - sql.with_conn(|conn| Ok(get_rowid_with_conn(context, conn, table, field, value))) - .unwrap_or_else(|_| 0) -} - -pub fn get_rowid_with_conn( - context: &Context, - conn: &Connection, - table: impl AsRef, - field: impl AsRef, - value: impl AsRef, -) -> u32 { +) -> std::result::Result { // 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. @@ -999,45 +426,18 @@ pub fn get_rowid_with_conn( field.as_ref(), ); - match conn.query_row(&query, params![value.as_ref()], |row| row.get::<_, u32>(0)) { - Ok(id) => id, - Err(err) => { - error!( - context, - "sql: Failed to retrieve rowid: {} in {}", err, query - ); - 0 - } - } -} -pub fn get_rowid2( - context: &Context, - sql: &Sql, - table: impl AsRef, - field: impl AsRef, - value: i64, - field2: impl AsRef, - value2: i32, -) -> u32 { - sql.start_stmt("get rowid2".to_string()); - sql.with_conn(|conn| { - Ok(get_rowid2_with_conn( - context, conn, table, field, value, field2, value2, - )) - }) - .unwrap_or_else(|_| 0) + conn.query_row(&query, params![value.as_ref()], |row| row.get::<_, u32>(0)) } -pub fn get_rowid2_with_conn( - context: &Context, - conn: &Connection, +pub fn get_rowid2( + conn: &mut Connection, table: impl AsRef, field: impl AsRef, value: i64, field2: impl AsRef, value2: i32, -) -> u32 { - match conn.query_row( +) -> std::result::Result { + conn.query_row( &format!( "SELECT id FROM {} WHERE {}={} AND {}={} ORDER BY id DESC", table.as_ref(), @@ -1046,30 +446,12 @@ pub fn get_rowid2_with_conn( field2.as_ref(), value2, ), - NO_PARAMS, + params![], |row| row.get::<_, u32>(0), - ) { - Ok(id) => id, - Err(err) => { - error!(context, "sql: Failed to retrieve rowid2: {}", err); - 0 - } - } + ) } -/// Removes from the database locally deleted messages that also don't -/// have a server UID. -fn prune_tombstones(context: &Context) -> Result<()> { - context.sql.execute( - "DELETE FROM msgs \ - WHERE (chat_id = ? OR hidden) \ - AND server_uid = 0", - params![DC_CHAT_ID_TRASH], - )?; - Ok(()) -} - -pub fn housekeeping(context: &Context) { +pub async fn housekeeping(context: &Context) { let mut files_in_use = HashSet::new(); let mut unreferenced_count = 0; @@ -1079,31 +461,35 @@ pub fn housekeeping(context: &Context) { &mut files_in_use, "SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;", Param::File, - ); + ) + .await; maybe_add_from_param( context, &mut files_in_use, "SELECT param FROM jobs;", Param::File, - ); + ) + .await; maybe_add_from_param( context, &mut files_in_use, "SELECT param FROM chats;", Param::ProfileImage, - ); + ) + .await; maybe_add_from_param( context, &mut files_in_use, "SELECT param FROM contacts;", Param::ProfileImage, - ); + ) + .await; context .sql .query_map( "SELECT value FROM config;", - params![], + paramsv![], |row| row.get::<_, String>(0), |rows| { for row in rows { @@ -1112,6 +498,7 @@ pub fn housekeeping(context: &Context) { Ok(()) }, ) + .await .unwrap_or_else(|err| { warn!(context, "sql: failed query: {}", err); }); @@ -1119,13 +506,13 @@ pub fn housekeeping(context: &Context) { info!(context, "{} files in use.", files_in_use.len(),); /* go through directory and delete unused files */ let p = context.get_blobdir(); - match std::fs::read_dir(p) { - Ok(dir_handle) => { + 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(); - for entry in dir_handle { + while let Some(entry) = dir_handle.next().await { if entry.is_err() { break; } @@ -1143,7 +530,7 @@ pub fn housekeeping(context: &Context) { unreferenced_count += 1; - if let Ok(stats) = std::fs::metadata(entry.path()) { + 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() @@ -1168,7 +555,7 @@ pub fn housekeeping(context: &Context) { entry.file_name() ); let path = entry.path(); - dc_delete_file(context, path); + dc_delete_file(context, path).await; } } Err(err) => { @@ -1181,10 +568,10 @@ pub fn housekeeping(context: &Context) { } } - if let Err(err) = prune_tombstones(context) { + if let Err(err) = prune_tombstones(context).await { warn!( context, - "Housekeeping: Cannot prune message tombstones: {}", err + "Houskeeping: Cannot prune message tombstones: {}", err ); } @@ -1213,7 +600,7 @@ fn maybe_add_file(files_in_use: &mut HashSet, file: impl AsRef) { files_in_use.insert(file.as_ref()[9..].into()); } -fn maybe_add_from_param( +async fn maybe_add_from_param( context: &Context, files_in_use: &mut HashSet, query: &str, @@ -1223,7 +610,7 @@ fn maybe_add_from_param( .sql .query_map( query, - NO_PARAMS, + paramsv![], |row| row.get::<_, String>(0), |rows| { for row in rows { @@ -1235,11 +622,677 @@ fn maybe_add_from_param( Ok(()) }, ) + .await .unwrap_or_else(|err| { warn!(context, "sql: failed to add_from_param: {}", err); }); } +#[allow(clippy::cognitive_complexity)] +async fn open( + context: &Context, + sql: &Sql, + dbfile: impl AsRef, + readonly: bool, +) -> crate::error::Result<()> { + if sql.is_open().await { + error!( + context, + "Cannot open, database \"{:?}\" already opened.", + dbfile.as_ref(), + ); + return Err(Error::SqlAlreadyOpen.into()); + } + + let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX; + if readonly { + open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY); + } else { + open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); + open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE); + } + + // this actually creates min_idle database handles just now. + // therefore, with_init() must not try to modify the database as otherwise + // we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle) + let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile.as_ref()) + .with_flags(open_flags) + .with_init(|c| { + c.execute_batch(&format!( + "PRAGMA secure_delete=on; PRAGMA busy_timeout = {};", + Duration::from_secs(10).as_millis() + ))?; + Ok(()) + }); + let pool = r2d2::Pool::builder() + .min_idle(Some(2)) + .max_size(10) + .connection_timeout(Duration::from_secs(60)) + .build(mgr) + .map_err(Error::ConnectionPool)?; + + { + *sql.pool.write().await = Some(pool); + } + + if !readonly { + // journal_mode is persisted, it is sufficient to change it only for one handle. + // (nb: execute() always returns errors for this PRAGMA call, just discard it. + // but even if execute() would handle errors more gracefully, we should continue on errors - + // systems might not be able to handle WAL, in which case the standard-journal is used. + // that may be not optimal, but better than not working at all :) + sql.execute("PRAGMA journal_mode=WAL;", paramsv![]) + .await + .ok(); + + let mut exists_before_update = false; + let mut dbversion_before_update: i32 = 0; + /* Init tables to dbversion=0 */ + if !sql.table_exists("config").await? { + info!( + context, + "First time init: creating tables in {:?}.", + dbfile.as_ref(), + ); + sql.execute( + "CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX config_index1 ON config (keyname);", + paramsv![], + ) + .await?; + sql.execute( + "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 '');", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);", + paramsv![], + ) + .await?; + sql.execute( + "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);", + paramsv![], + ) + .await?; + sql.execute( + "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 '');", + paramsv![], + ) + .await?; + sql.execute("CREATE INDEX chats_index1 ON chats (grpid);", paramsv![]) + .await?; + sql.execute( + "CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);", + paramsv![], + ) + .await?; + sql.execute( + "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');", + paramsv![], + ) + .await?; + sql.execute( + "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 '');", + paramsv![], + ) + .await?; + sql.execute("CREATE INDEX msgs_index1 ON msgs (rfc724_mid);", paramsv![]) + .await?; + sql.execute("CREATE INDEX msgs_index2 ON msgs (chat_id);", paramsv![]) + .await?; + sql.execute("CREATE INDEX msgs_index3 ON msgs (timestamp);", paramsv![]) + .await?; + sql.execute("CREATE INDEX msgs_index4 ON msgs (state);", paramsv![]) + .await?; + sql.execute( + "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');", + paramsv![], + ) + .await?; + sql.execute( + "CREATE TABLE jobs (\ + id INTEGER PRIMARY KEY AUTOINCREMENT, \ + added_timestamp INTEGER, \ + desired_timestamp INTEGER DEFAULT 0, \ + action INTEGER, \ + foreign_id INTEGER, \ + param TEXT DEFAULT '');", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX jobs_index1 ON jobs (desired_timestamp);", + paramsv![], + ) + .await?; + if !sql.table_exists("config").await? + || !sql.table_exists("contacts").await? + || !sql.table_exists("chats").await? + || !sql.table_exists("chats_contacts").await? + || !sql.table_exists("msgs").await? + || !sql.table_exists("jobs").await? + { + error!( + context, + "Cannot create tables in new database \"{:?}\".", + dbfile.as_ref(), + ); + // cannot create the tables - maybe we cannot write? + return Err(Error::SqlFailedToOpen.into()); + } else { + sql.set_raw_config_int(context, "dbversion", 0).await?; + } + } else { + exists_before_update = true; + dbversion_before_update = sql + .get_raw_config_int(context, "dbversion") + .await + .unwrap_or_default(); + } + + // (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 mut dbversion = dbversion_before_update; + let mut recalc_fingerprints = false; + let mut update_icons = false; + + if dbversion < 1 { + info!(context, "[migration] v1"); + sql.execute( + "CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX leftgrps_index1 ON leftgrps (grpid);", + paramsv![], + ) + .await?; + dbversion = 1; + sql.set_raw_config_int(context, "dbversion", 1).await?; + } + if dbversion < 2 { + info!(context, "[migration] v2"); + sql.execute( + "ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';", + paramsv![], + ) + .await?; + dbversion = 2; + sql.set_raw_config_int(context, "dbversion", 2).await?; + } + if dbversion < 7 { + info!(context, "[migration] v7"); + sql.execute( + "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);", + paramsv![], + ) + .await?; + dbversion = 7; + sql.set_raw_config_int(context, "dbversion", 7).await?; + } + if dbversion < 10 { + info!(context, "[migration] v10"); + sql.execute( + "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);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);", + paramsv![], + ) + .await?; + dbversion = 10; + sql.set_raw_config_int(context, "dbversion", 10).await?; + } + if dbversion < 12 { + info!(context, "[migration] v12"); + sql.execute( + "CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);", + paramsv![], + ) + .await?; + dbversion = 12; + sql.set_raw_config_int(context, "dbversion", 12).await?; + } + if dbversion < 17 { + info!(context, "[migration] v17"); + sql.execute( + "ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute("CREATE INDEX chats_index2 ON chats (archived);", paramsv![]) + .await?; + sql.execute( + "ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute("CREATE INDEX msgs_index5 ON msgs (starred);", paramsv![]) + .await?; + dbversion = 17; + sql.set_raw_config_int(context, "dbversion", 17).await?; + } + if dbversion < 18 { + info!(context, "[migration] v18"); + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN gossip_key;", + paramsv![], + ) + .await?; + dbversion = 18; + sql.set_raw_config_int(context, "dbversion", 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("DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;", paramsv![]) + .await?; + sql.execute( + "CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + dbversion = 27; + sql.set_raw_config_int(context, "dbversion", 27).await?; + } + if dbversion < 34 { + info!(context, "[migration] v34"); + sql.execute( + "ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);", + paramsv![], + ) + .await?; + recalc_fingerprints = true; + dbversion = 34; + sql.set_raw_config_int(context, "dbversion", 34).await?; + } + if dbversion < 39 { + info!(context, "[migration] v39"); + sql.execute( + "CREATE TABLE tokens ( id INTEGER PRIMARY KEY, namespc INTEGER DEFAULT 0, foreign_id INTEGER DEFAULT 0, token TEXT DEFAULT '', timestamp INTEGER DEFAULT 0);", + paramsv![] + ).await?; + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN verified_key;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);", + paramsv![], + ) + .await?; + dbversion = 39; + sql.set_raw_config_int(context, "dbversion", 39).await?; + } + if dbversion < 40 { + info!(context, "[migration] v40"); + sql.execute( + "ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + dbversion = 40; + sql.set_raw_config_int(context, "dbversion", 40).await?; + } + if dbversion < 44 { + info!(context, "[migration] v44"); + sql.execute("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", paramsv![]) + .await?; + dbversion = 44; + sql.set_raw_config_int(context, "dbversion", 44).await?; + } + if dbversion < 46 { + info!(context, "[migration] v46"); + sql.execute( + "ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE msgs ADD COLUMN mime_references TEXT;", + paramsv![], + ) + .await?; + dbversion = 46; + sql.set_raw_config_int(context, "dbversion", 46).await?; + } + if dbversion < 47 { + info!(context, "[migration] v47"); + sql.execute( + "ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + dbversion = 47; + sql.set_raw_config_int(context, "dbversion", 47).await?; + } + if dbversion < 48 { + info!(context, "[migration] v48"); + // NOTE: move_state is not used anymore + sql.execute( + "ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;", + paramsv![], + ) + .await?; + + dbversion = 48; + sql.set_raw_config_int(context, "dbversion", 48).await?; + } + if dbversion < 49 { + info!(context, "[migration] v49"); + sql.execute( + "ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + dbversion = 49; + sql.set_raw_config_int(context, "dbversion", 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(context, "show_emails", ShowEmails::All as i32) + .await?; + } + dbversion = 50; + sql.set_raw_config_int(context, "dbversion", 50).await?; + } + if dbversion < 53 { + info!(context, "[migration] v53"); + // the messages containing _only_ locations + // are also added to the database as _hidden_. + sql.execute( + "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);", + paramsv![] + ).await?; + sql.execute( + "CREATE INDEX locations_index1 ON locations (from_id);", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX locations_index2 ON locations (timestamp);", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX chats_index3 ON chats (locations_send_until);", + paramsv![], + ) + .await?; + dbversion = 53; + sql.set_raw_config_int(context, "dbversion", 53).await?; + } + if dbversion < 54 { + info!(context, "[migration] v54"); + sql.execute( + "ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.execute( + "CREATE INDEX msgs_index6 ON msgs (location_id);", + paramsv![], + ) + .await?; + dbversion = 54; + sql.set_raw_config_int(context, "dbversion", 54).await?; + } + if dbversion < 55 { + info!(context, "[migration] v55"); + sql.execute( + "ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.set_raw_config_int(context, "dbversion", 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( + "CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);", + paramsv![], + ).await?; + sql.execute( + "CREATE INDEX devmsglabels_index1 ON devmsglabels (label);", + paramsv![], + ) + .await?; + if exists_before_update && sql.get_raw_config_int(context, "bcc_self").await.is_none() { + sql.set_raw_config_int(context, "bcc_self", 1).await?; + } + sql.set_raw_config_int(context, "dbversion", 59).await?; + } + if dbversion < 60 { + info!(context, "[migration] v60"); + sql.execute( + "ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.set_raw_config_int(context, "dbversion", 60).await?; + } + if dbversion < 61 { + info!(context, "[migration] v61"); + sql.execute( + "ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + update_icons = true; + sql.set_raw_config_int(context, "dbversion", 61).await?; + } + if dbversion < 62 { + info!(context, "[migration] v62"); + sql.execute( + "ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;", + paramsv![], + ) + .await?; + sql.set_raw_config_int(context, "dbversion", 62).await?; + } + if dbversion < 63 { + info!(context, "[migration] v63"); + sql.execute("UPDATE chats SET grpid='' WHERE type=100", paramsv![]) + .await?; + sql.set_raw_config_int(context, "dbversion", 63).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 addrs = sql + .query_map( + "SELECT addr FROM acpeerstates;", + paramsv![], + |row| row.get::<_, String>(0), + |addrs| { + addrs + .collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; + for addr in &addrs { + if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await { + peerstate.recalc_fingerprint(); + peerstate.save_to_db(sql, false).await?; + } + } + } + if update_icons { + update_saved_messages_icon(context).await?; + update_device_icon(context).await?; + } + } + + info!(context, "Opened {:?}.", dbfile.as_ref(),); + + Ok(()) +} + +/// Removes from the database locally deleted messages that also don't +/// have a server UID. +async fn prune_tombstones(context: &Context) -> Result<()> { + context + .sql + .execute( + "DELETE FROM msgs \ + WHERE (chat_id = ? OR hidden) \ + AND server_uid = 0", + paramsv![DC_CHAT_ID_TRASH], + ) + .await?; + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/stock.rs b/src/stock.rs index c5ff765f0..78d763e58 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -197,7 +197,7 @@ impl StockMessage { impl Context { /// Set the stock string for the [StockMessage]. /// - pub fn set_stock_translation( + pub async fn set_stock_translation( &self, id: StockMessage, stockstring: String, @@ -218,7 +218,7 @@ impl Context { } self.translated_stockstrings .write() - .unwrap() + .await .insert(id as usize, stockstring); Ok(()) } @@ -227,11 +227,11 @@ impl Context { /// /// Return a translation (if it was set with set_stock_translation before) /// or a default (English) string. - pub fn stock_str(&self, id: StockMessage) -> Cow { + pub async fn stock_str(&self, id: StockMessage) -> Cow<'_, str> { match self .translated_stockstrings .read() - .unwrap() + .await .get(&(id as usize)) { Some(ref x) => Cow::Owned((*x).to_string()), @@ -244,8 +244,9 @@ impl Context { /// This replaces both the *first* `%1$s`, `%1$d` and `%1$@` /// placeholders with the provided string. /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) - pub fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef) -> String { + pub async fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef) -> String { self.stock_str(id) + .await .replacen("%1$s", insert.as_ref(), 1) .replacen("%1$d", insert.as_ref(), 1) .replacen("%1$@", insert.as_ref(), 1) @@ -255,8 +256,9 @@ impl Context { /// /// Like [Context::stock_string_repl_str] but substitute the placeholders /// with an integer. - pub fn stock_string_repl_int(&self, id: StockMessage, insert: i32) -> String { + pub async fn stock_string_repl_int(&self, id: StockMessage, insert: i32) -> String { self.stock_string_repl_str(id, format!("{}", insert).as_str()) + .await } /// Return stock string, replacing 2 placeholders with provided string. @@ -265,13 +267,14 @@ impl Context { /// placeholders with the string in `insert` and does the same for /// `%2$s`, `%2$d` and `%2$@` for `insert2`. /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) - pub fn stock_string_repl_str2( + pub async fn stock_string_repl_str2( &self, id: StockMessage, insert: impl AsRef, insert2: impl AsRef, ) -> String { self.stock_str(id) + .await .replacen("%1$s", insert.as_ref(), 1) .replacen("%1$d", insert.as_ref(), 1) .replacen("%1$@", insert.as_ref(), 1) @@ -297,7 +300,7 @@ impl Context { /// used as the second parameter to [StockMessage::MsgActionByUser] with /// again the original stock string being used as the first parameter, /// resulting in a string like "Member Alice added by Bob.". - pub fn stock_system_msg( + pub async fn stock_system_msg( &self, id: StockMessage, param1: impl AsRef, @@ -305,9 +308,11 @@ impl Context { from_id: u32, ) -> String { let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember { - let contact_id = Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown); + let contact_id = + Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown).await; if contact_id != 0 { Contact::get_by_id(self, contact_id) + .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default() } else { @@ -317,52 +322,60 @@ impl Context { param1.as_ref().to_string() }; - let action = self.stock_string_repl_str2(id, insert1, param2.as_ref().to_string()); + let action = self + .stock_string_repl_str2(id, insert1, param2.as_ref().to_string()) + .await; let action1 = action.trim_end_matches('.'); match from_id { 0 => action, - 1 => self.stock_string_repl_str(StockMessage::MsgActionByMe, action1), // DC_CONTACT_ID_SELF + 1 => { + self.stock_string_repl_str(StockMessage::MsgActionByMe, action1) + .await + } // DC_CONTACT_ID_SELF _ => { let displayname = Contact::get_by_id(self, from_id) + .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default(); self.stock_string_repl_str2(StockMessage::MsgActionByUser, action1, &displayname) + .await } } } - pub fn update_device_chats(&self) -> Result<(), Error> { + pub async fn update_device_chats(&self) -> Result<(), Error> { // check for the LAST added device message - if it is present, we can skip message creation. // this is worthwhile as this function is typically called // by the ui on every probram start or even on every opening of the chatlist. - if chat::was_device_msg_ever_added(&self, "core-welcome")? { + if chat::was_device_msg_ever_added(&self, "core-welcome").await? { return Ok(()); } // create saved-messages chat; // we do this only once, if the user has deleted the chat, he can recreate it manually. - if !self.sql.get_raw_config_bool(&self, "self-chat-added") { + if !self.sql.get_raw_config_bool(&self, "self-chat-added").await { self.sql - .set_raw_config_bool(&self, "self-chat-added", true)?; - chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF)?; + .set_raw_config_bool(&self, "self-chat-added", true) + .await?; + chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF).await?; } // add welcome-messages. by the label, this is done only once, // if the user has deleted the message or the chat, it is not added again. let mut msg = Message::new(Viewtype::Text); - msg.text = Some(self.stock_str(DeviceMessagesHint).to_string()); - chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg))?; + msg.text = Some(self.stock_str(DeviceMessagesHint).await.to_string()); + chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg)).await?; let image = include_bytes!("../assets/welcome-image.jpg"); - let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image)?; + let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image).await?; let mut msg = Message::new(Viewtype::Image); msg.param.set(Param::File, blob.as_name()); - chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg))?; + chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg)).await?; let mut msg = Message::new(Viewtype::Text); - msg.text = Some(self.stock_str(WelcomeMessage).to_string()); - chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg))?; + msg.text = Some(self.stock_str(WelcomeMessage).await.to_string()); + chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg)).await?; Ok(()) } } @@ -388,165 +401,191 @@ mod tests { assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); } - #[test] - fn test_set_stock_translation() { - let t = dummy_context(); + #[async_std::test] + async fn test_set_stock_translation() { + let t = dummy_context().await; t.ctx .set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) + .await .unwrap(); - assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "xyz") + assert_eq!(t.ctx.stock_str(StockMessage::NoMessages).await, "xyz") } - #[test] - fn test_set_stock_translation_wrong_replacements() { - let t = dummy_context(); + #[async_std::test] + async fn test_set_stock_translation_wrong_replacements() { + let t = dummy_context().await; assert!(t .ctx .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) + .await .is_err()); assert!(t .ctx .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) + .await .is_err()); } - #[test] - fn test_stock_str() { - let t = dummy_context(); - assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages."); + #[async_std::test] + async fn test_stock_str() { + let t = dummy_context().await; + assert_eq!( + t.ctx.stock_str(StockMessage::NoMessages).await, + "No messages." + ); } - #[test] - fn test_stock_string_repl_str() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_string_repl_str() { + let t = dummy_context().await; // uses %1$s substitution assert_eq!( t.ctx - .stock_string_repl_str(StockMessage::MsgAddMember, "Foo"), + .stock_string_repl_str(StockMessage::MsgAddMember, "Foo") + .await, "Member Foo added." ); // We have no string using %1$d to test... } - #[test] - fn test_stock_string_repl_int() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_string_repl_int() { + let t = dummy_context().await; assert_eq!( - t.ctx.stock_string_repl_int(StockMessage::MsgAddMember, 42), + t.ctx + .stock_string_repl_int(StockMessage::MsgAddMember, 42) + .await, "Member 42 added." ); } - #[test] - fn test_stock_string_repl_str2() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_string_repl_str2() { + let t = dummy_context().await; assert_eq!( t.ctx - .stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"), + .stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar") + .await, "Could not connect to foo: bar" ); } - #[test] - fn test_stock_system_msg_simple() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_system_msg_simple() { + let t = dummy_context().await; assert_eq!( t.ctx - .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0), + .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0) + .await, "Location streaming enabled." ) } - #[test] - fn test_stock_system_msg_add_member_by_me() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_system_msg_add_member_by_me() { + let t = dummy_context().await; assert_eq!( - t.ctx.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - DC_CONTACT_ID_SELF - ), + t.ctx + .stock_system_msg( + StockMessage::MsgAddMember, + "alice@example.com", + "", + DC_CONTACT_ID_SELF + ) + .await, "Member alice@example.com added by me." ) } - #[test] - fn test_stock_system_msg_add_member_by_me_with_displayname() { - let t = dummy_context(); - Contact::create(&t.ctx, "Alice", "alice@example.com").expect("failed to create contact"); + #[async_std::test] + async fn test_stock_system_msg_add_member_by_me_with_displayname() { + let t = dummy_context().await; + Contact::create(&t.ctx, "Alice", "alice@example.com") + .await + .expect("failed to create contact"); assert_eq!( - t.ctx.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - DC_CONTACT_ID_SELF - ), + t.ctx + .stock_system_msg( + StockMessage::MsgAddMember, + "alice@example.com", + "", + DC_CONTACT_ID_SELF + ) + .await, "Member Alice (alice@example.com) added by me." ); } - #[test] - fn test_stock_system_msg_add_member_by_other_with_displayname() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_system_msg_add_member_by_other_with_displayname() { + let t = dummy_context().await; let contact_id = { Contact::create(&t.ctx, "Alice", "alice@example.com") + .await .expect("Failed to create contact Alice"); - Contact::create(&t.ctx, "Bob", "bob@example.com").expect("failed to create bob") + Contact::create(&t.ctx, "Bob", "bob@example.com") + .await + .expect("failed to create bob") }; assert_eq!( - t.ctx.stock_system_msg( - StockMessage::MsgAddMember, - "alice@example.com", - "", - contact_id, - ), + t.ctx + .stock_system_msg( + StockMessage::MsgAddMember, + "alice@example.com", + "", + contact_id, + ) + .await, "Member Alice (alice@example.com) added by Bob (bob@example.com)." ); } - #[test] - fn test_stock_system_msg_grp_name() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_system_msg_grp_name() { + let t = dummy_context().await; assert_eq!( - t.ctx.stock_system_msg( - StockMessage::MsgGrpName, - "Some chat", - "Other chat", - DC_CONTACT_ID_SELF - ), + t.ctx + .stock_system_msg( + StockMessage::MsgGrpName, + "Some chat", + "Other chat", + DC_CONTACT_ID_SELF + ) + .await, "Group name changed from \"Some chat\" to \"Other chat\" by me." ) } - #[test] - fn test_stock_system_msg_grp_name_other() { - let t = dummy_context(); + #[async_std::test] + async fn test_stock_system_msg_grp_name_other() { + let t = dummy_context().await; let id = Contact::create(&t.ctx, "Alice", "alice@example.com") + .await .expect("failed to create contact"); assert_eq!( t.ctx - .stock_system_msg(StockMessage::MsgGrpName, "Some chat", "Other chat", id,), + .stock_system_msg(StockMessage::MsgGrpName, "Some chat", "Other chat", id) + .await, "Group name changed from \"Some chat\" to \"Other chat\" by Alice (alice@example.com)." ) } - #[test] - fn test_update_device_chats() { - let t = dummy_context(); - t.ctx.update_device_chats().ok(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + #[async_std::test] + async fn test_update_device_chats() { + let t = dummy_context().await; + t.ctx.update_device_chats().await.ok(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 2); - chats.get_chat_id(0).delete(&t.ctx).ok(); - chats.get_chat_id(1).delete(&t.ctx).ok(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + chats.get_chat_id(0).delete(&t.ctx).await.ok(); + chats.get_chat_id(1).delete(&t.ctx).await.ok(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // a subsequent call to update_device_chats() must not re-add manally deleted messages or chats - t.ctx.update_device_chats().ok(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); + t.ctx.update_device_chats().await.ok(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 815b50b7a..394fafc02 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -5,9 +5,8 @@ use tempfile::{tempdir, TempDir}; use crate::config::Config; -use crate::context::{Context, ContextCallback}; +use crate::context::Context; use crate::dc_tools::EmailAddress; -use crate::events::Event; use crate::key::{self, DcKey}; /// A Context and temporary directory. @@ -25,14 +24,10 @@ pub(crate) struct TestContext { /// "db.sqlite" in the [TestContext.dir] directory. /// /// [Context]: crate::context::Context -pub(crate) fn test_context(callback: Option>) -> TestContext { +pub(crate) async fn test_context() -> TestContext { let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); - let cb: Box = match callback { - Some(cb) => cb, - None => Box::new(|_, _| ()), - }; - let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap(); + let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap(); TestContext { ctx, dir } } @@ -41,17 +36,8 @@ pub(crate) fn test_context(callback: Option>) -> TestContex /// The context will be opened and use the SQLite database as /// specified in [test_context] but there is no callback hooked up, /// i.e. [Context::call_cb] will always return `0`. -pub(crate) fn dummy_context() -> TestContext { - test_context(Some(Box::new(logging_cb))) -} - -pub(crate) fn logging_cb(_ctx: &Context, evt: Event) { - match evt { - Event::Info(msg) => println!("I: {}", msg), - Event::Warning(msg) => eprintln!("=== WARNING ===\n{}\n===============", msg), - Event::Error(msg) => eprintln!("\n===================== ERROR =====================\n{}\n=================================================\n", msg), - _ => (), - } +pub(crate) async fn dummy_context() -> TestContext { + test_context().await } /// Load a pre-generated keypair for alice@example.com from disk. @@ -77,11 +63,13 @@ pub(crate) fn alice_keypair() -> key::KeyPair { /// Creates Alice with a pre-generated keypair. /// /// Returns the address of the keypair created (alice@example.com). -pub(crate) fn configure_alice_keypair(ctx: &Context) -> String { +pub(crate) async fn configure_alice_keypair(ctx: &Context) -> String { let keypair = alice_keypair(); ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string())) + .await .unwrap(); key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default) + .await .expect("Failed to save Alice's key"); keypair.addr.to_string() } diff --git a/src/token.rs b/src/token.rs index 11ae53f0c..b3a89d5b4 100644 --- a/src/token.rs +++ b/src/token.rs @@ -9,7 +9,6 @@ use deltachat_derive::*; use crate::chat::ChatId; use crate::context::Context; use crate::dc_tools::*; -use crate::sql; /// Token namespace #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)] @@ -28,37 +27,46 @@ impl Default for Namespace { /// Creates a new token and saves it into the database. /// Returns created token. -pub fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { +pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { // foreign_id may be 0 let token = dc_create_id(); - sql::execute( - context, - &context.sql, - "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", - params![namespace, foreign_id, &token, time()], - ) - .ok(); + context + .sql + .execute( + "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", + paramsv![namespace, foreign_id, token, time()], + ) + .await + .ok(); token } -pub fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option { - context.sql.query_get_value::<_, String>( - context, - "SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;", - params![namespace, foreign_id], - ) +pub async fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option { + context + .sql + .query_get_value::( + context, + "SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;", + paramsv![namespace, foreign_id], + ) + .await } -pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { - lookup(context, namespace, foreign_id).unwrap_or_else(|| save(context, namespace, foreign_id)) +pub async fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { + if let Some(token) = lookup(context, namespace, foreign_id).await { + return token; + } + + save(context, namespace, foreign_id).await } -pub fn exists(context: &Context, namespace: Namespace, token: &str) -> bool { +pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> bool { context .sql .exists( "SELECT id FROM tokens WHERE namespc=? AND token=?;", - params![namespace, token], + paramsv![namespace, token], ) + .await .unwrap_or_default() } diff --git a/tests/stress.rs b/tests/stress.rs index ed562ee77..ee00e713f 100644 --- a/tests/stress.rs +++ b/tests/stress.rs @@ -2,14 +2,16 @@ use deltachat::config; use deltachat::context::*; -use deltachat::Event; use tempfile::{tempdir, TempDir}; /* some data used for testing ******************************************************************************/ -fn stress_functions(context: &Context) { - let res = context.get_config(config::Config::SysConfigKeys).unwrap(); +async fn stress_functions(context: &Context) { + let res = context + .get_config(config::Config::SysConfigKeys) + .await + .unwrap(); assert!(!res.contains(" probably_never_a_key ")); assert!(res.contains(" addr ")); @@ -90,23 +92,21 @@ fn stress_functions(context: &Context) { // free(qr.cast()); } -fn cb(_context: &Context, _event: Event) {} - #[allow(dead_code)] struct TestContext { ctx: Context, dir: TempDir, } -fn create_test_context() -> TestContext { +async fn create_test_context() -> TestContext { let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); - let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap(); + let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap(); TestContext { ctx, dir } } -#[test] -fn test_stress_tests() { - let context = create_test_context(); - stress_functions(&context.ctx); +#[async_std::test] +async fn test_stress_tests() { + let context = create_test_context().await; + stress_functions(&context.ctx).await; }