diff --git a/Cargo.lock b/Cargo.lock index 5cc92eaf9..45c84c585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "adler" -version = "0.2.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "adler32" @@ -113,6 +113,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "ahash" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877" +dependencies = [ + "getrandom 0.2.2", + "once_cell", + "version_check 0.9.3", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -142,9 +153,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" [[package]] name = "arrayref" @@ -176,9 +187,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59740d83946db6a5af71ae25ddf9562c2b176b2ca42cf99a455f09f4a220d6b9" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" dependencies = [ "concurrent-queue", "event-listener", @@ -224,9 +235,9 @@ dependencies = [ [[package]] name = "async-h1" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c68a75f812ff0f299e142c06dd0c34e3295a594d935e61eeb6c77041d1d4dc" +checksum = "cc5142de15b549749cce62923a50714b0d7b77f5090ced141599e78899865451" dependencies = [ "async-channel", "async-dup", @@ -237,7 +248,7 @@ dependencies = [ "httparse", "lazy_static", "log", - "pin-project 1.0.4", + "pin-project 1.0.5", ] [[package]] @@ -305,13 +316,13 @@ dependencies = [ [[package]] name = "async-process" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8cea09c1fb10a317d1b5af8024eeba256d6554763e85ecd90ff8df31c7bbda" +checksum = "ef37b86e2fa961bae5a4d212708ea0154f904ce31d1a4a7f47e1bbc33a0c040b" dependencies = [ "async-io", "blocking", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "event-listener", "futures-lite", "once_cell", @@ -354,7 +365,7 @@ dependencies = [ "async-mutex", "async-process", "blocking", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", "futures-channel", "futures-core", "futures-io", @@ -365,7 +376,7 @@ dependencies = [ "memchr", "num_cpus", "once_cell", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "pin-utils", "slab", "wasm-bindgen-futures", @@ -373,9 +384,9 @@ dependencies = [ [[package]] name = "async-std-resolver" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c0273ea9c8f9e2997e4c36a003fd5a9fad4a35dc58a8832823c0128ce1d54" +checksum = "c7c9658b625587d155723bfefbd36d5e089a2f3ba7146bf0818a06568ea67826" dependencies = [ "async-std", "async-trait", @@ -405,15 +416,24 @@ checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" [[package]] name = "async-trait" -version = "0.1.42" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.0.0" @@ -452,7 +472,7 @@ dependencies = [ "addr2line", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.4.3", + "miniz_oxide 0.4.4", "object", "rustc-demangle", ] @@ -517,6 +537,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -590,9 +622,9 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ "lazy_static", "memchr", @@ -617,10 +649,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] -name = "bumpalo" -version = "3.5.0" +name = "build_const" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byte-pool" @@ -628,21 +666,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38e98299d518ec351ca016363e0cbfc77059dcd08dfa9700d15e405536097a" dependencies = [ - "crossbeam-queue", + "crossbeam-queue 0.2.3", "stable_deref_trait", ] [[package]] name = "bytemuck" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a4bad0c5981acc24bc09e532f35160f952e35422603f0563cd7a73c2c2e65a0" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cache-padded" @@ -672,9 +716,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfb-mode" @@ -770,6 +814,17 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom 5.1.2", + "serde", +] + [[package]] name = "const_fn" version = "0.4.5" @@ -784,19 +839,19 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "cookie" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "aes-gcm", - "base64 0.12.3", + "base64 0.13.0", "hkdf", "hmac", "percent-encoding", - "rand 0.7.3", + "rand 0.8.3", "sha2", - "time 0.2.24", - "version_check 0.9.2", + "time 0.2.26", + "version_check 0.9.3", ] [[package]] @@ -827,6 +882,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc24" version = "0.1.6" @@ -844,16 +908,16 @@ dependencies = [ [[package]] name = "criterion" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8" +checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23" dependencies = [ "atty", "cast", "clap", "criterion-plot", "csv", - "itertools", + "itertools 0.10.0", "lazy_static", "num-traits", "oorandom", @@ -875,7 +939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" dependencies = [ "cast", - "itertools", + "itertools 0.9.0", ] [[package]] @@ -885,7 +949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", ] [[package]] @@ -896,18 +960,17 @@ checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", ] [[package]] name = "crossbeam-epoch" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" dependencies = [ "cfg-if 1.0.0", - "const_fn", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", "lazy_static", "memoffset", "scopeguard", @@ -924,6 +987,16 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.3", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -937,9 +1010,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg 1.0.1", "cfg-if 1.0.0", @@ -958,9 +1031,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d58633299b24b515ac72a3f869f8b91306a3cec616a602843a383acd6f9e97" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ "bstr", "csv-core", @@ -980,9 +1053,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bcb9d7dcbf7002aaffbb53eac22906b64cdcc127971dcc387d8eb7c95d5560" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" dependencies = [ "quote", "syn", @@ -1046,10 +1119,34 @@ dependencies = [ ] [[package]] -name = "data-encoding" -version = "2.3.1" +name = "dashmap" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "deadpool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d" +dependencies = [ + "async-trait", + "config", + "crossbeam-queue 0.3.1", + "num_cpus", + "serde", + "tokio", +] [[package]] name = "deflate" @@ -1081,7 +1178,6 @@ dependencies = [ "charset", "chrono", "criterion", - "deltachat_derive", "dirs 3.0.1", "email", "encoded-words", @@ -1091,15 +1187,17 @@ dependencies = [ "hex", "image", "indexmap", - "itertools", + "itertools 0.9.0", "kamadak-exif", "lettre_email", "libc", + "libsqlite3-sys", "log", "mailparse", "native-tls", "num-derive", "num-traits", + "num_cpus", "once_cell", "percent-encoding", "pgp", @@ -1107,11 +1205,8 @@ dependencies = [ "pretty_env_logger", "proptest", "quick-xml", - "r2d2", - "r2d2_sqlite", "rand 0.7.3", "regex", - "rusqlite", "rust-hsluv", "rustyline", "sanitize-filename", @@ -1120,6 +1215,7 @@ dependencies = [ "sha-1", "sha2", "smallvec", + "sqlx", "stop-token", "strum", "strum_macros", @@ -1131,14 +1227,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "deltachat_derive" -version = "2.0.0" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "deltachat_ffi" version = "1.51.0" @@ -1242,6 +1330,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "ed25519" version = "1.0.3" @@ -1283,7 +1377,7 @@ dependencies = [ "lazy_static", "rand 0.7.3", "time 0.1.44", - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -1366,9 +1460,9 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ "cfg-if 1.0.0", ] @@ -1419,18 +1513,6 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" -[[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" @@ -1457,20 +1539,20 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.4", + "redox_syscall 0.2.5", "winapi", ] [[package]] name = "flate2" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" dependencies = [ "cfg-if 1.0.0", "crc32fast", "libc", - "miniz_oxide 0.4.3", + "miniz_oxide 0.4.4", ] [[package]] @@ -1496,19 +1578,25 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", ] [[package]] -name = "futures" -version = "0.3.12" +name = "funty" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" dependencies = [ "futures-channel", "futures-core", @@ -1521,9 +1609,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" dependencies = [ "futures-core", "futures-sink", @@ -1531,15 +1619,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" dependencies = [ "futures-core", "futures-task", @@ -1548,9 +1636,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" [[package]] name = "futures-lite" @@ -1563,15 +1651,15 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "waker-fn", ] [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -1581,24 +1669,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" dependencies = [ "futures-channel", "futures-core", @@ -1607,7 +1692,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "pin-utils", "proc-macro-hack", "proc-macro-nested", @@ -1621,7 +1706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ "typenum", - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -1658,9 +1743,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02efba560f227847cb41463a7395c514d127d4f74fff12ef0137fff1b84b96c4" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" dependencies = [ "color_quant", "weezl", @@ -1697,7 +1782,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash", + "ahash 0.4.7", ] [[package]] @@ -1729,9 +1814,9 @@ dependencies = [ [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" @@ -1776,14 +1861,18 @@ dependencies = [ [[package]] name = "http-client" -version = "6.2.0" +version = "6.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010092b71b94ee49293995625ce7a607778b8b4099c8088fa84fd66bd3e0f21c" +checksum = "5566ecc26bc6b04e773e680d66141fced78e091ad818e420d726c152b05a64ff" dependencies = [ "async-h1", "async-native-tls", "async-std", "async-trait", + "cfg-if 1.0.0", + "dashmap", + "deadpool", + "futures", "http-types", "log", ] @@ -1801,7 +1890,7 @@ dependencies = [ "cookie", "futures-lite", "infer", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "rand 0.7.3", "serde", "serde_json", @@ -1812,9 +1901,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" [[package]] name = "human-panic" @@ -1848,9 +1937,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" dependencies = [ "matches", "unicode-bidi", @@ -1859,9 +1948,9 @@ dependencies = [ [[package]] name = "image" -version = "0.23.12" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce04077ead78e39ae8610ad26216aed811996b043d47beed5090db674f9e9b5" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" dependencies = [ "bytemuck", "byteorder", @@ -1885,9 +1974,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ "autocfg 1.0.1", "hashbrown", @@ -1914,7 +2003,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" dependencies = [ - "socket2", + "socket2 0.3.19", "widestring", "winapi", "winreg", @@ -1929,6 +2018,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.7" @@ -1937,18 +2035,15 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "jpeg-decoder" -version = "0.1.20" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3" -dependencies = [ - "byteorder", -] +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.46" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" dependencies = [ "wasm-bindgen", ] @@ -2012,22 +2107,22 @@ dependencies = [ [[package]] name = "lexical-core" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" dependencies = [ "arrayvec", "bitflags", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "ryu", "static_assertions", ] [[package]] name = "libc" -version = "0.2.82" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" [[package]] name = "libm" @@ -2063,11 +2158,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -2090,6 +2186,12 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "match_cfg" version = "0.1.0" @@ -2161,9 +2263,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", "autocfg 1.0.1", @@ -2195,12 +2297,12 @@ dependencies = [ [[package]] name = "nb-connect" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998" +checksum = "a19900e7eee95eb2b3c2e26d12a874cc80aaf750e31be6fcbe743ead369fa45d" dependencies = [ "libc", - "winapi", + "socket2 0.4.0", ] [[package]] @@ -2234,7 +2336,20 @@ checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ "lexical-core", "memchr", - "version_check 0.9.2", + "version_check 0.9.3", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check 0.9.3", ] [[package]] @@ -2337,9 +2452,9 @@ checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "oorandom" @@ -2355,15 +2470,15 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.32" +version = "0.10.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", - "lazy_static", "libc", + "once_cell", "openssl-sys", ] @@ -2375,18 +2490,18 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.13.0+1.1.1i" +version = "111.14.0+1.1.1j" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045e4dc48af57aad93d665885789b43222ae26f4886494da12d1ed58d309dcb6" +checksum = "055b569b5bd7e5462a1700f595c7c7d487691d73b5ce064176af7f9f0cbb80a9" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.60" +version = "0.9.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" dependencies = [ "autocfg 1.0.1", "cc", @@ -2442,23 +2557,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.1.57", + "redox_syscall 0.2.5", "smallvec", "winapi", ] [[package]] name = "pem" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c220d01f863d13d96ca82359d1e81e64a7c6bf0637bcde7b2349630addf0c6" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" dependencies = [ "base64 0.13.0", "once_cell", @@ -2531,11 +2646,11 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b70b68509f17aa2857863b6fa00bf21fc93674c7a8893de2f469f6aa7ca2f2" +checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" dependencies = [ - "pin-project-internal 1.0.4", + "pin-project-internal 1.0.5", ] [[package]] @@ -2551,9 +2666,9 @@ dependencies = [ [[package]] name = "pin-project-internal" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa25a6393f22ce819b0f50e0be89287292fda8d425be38ee0ca14c4931d9e71" +checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" dependencies = [ "proc-macro2", "quote", @@ -2562,15 +2677,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" [[package]] name = "pin-utils" @@ -2586,16 +2701,32 @@ checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "plotters" -version = "0.2.15" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d1685fbe7beba33de0330629da9d955ac75bd54f33d7b79f9a895590124f6bb" +checksum = "45ca0ae5f169d0917a7c7f5a9c1a3d3d9598f18f529dd2b8373ed988efea307a" dependencies = [ - "js-sys", "num-traits", + "plotters-backend", + "plotters-svg", "wasm-bindgen", "web-sys", ] +[[package]] +name = "plotters-backend" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590" + +[[package]] +name = "plotters-svg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.16.8" @@ -2610,11 +2741,11 @@ dependencies = [ [[package]] name = "polling" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4" +checksum = "4fc12d774e799ee9ebae13f4076ca003b40d18a11ac0f3641e6f899618580b7b" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", "log", "wepoll-sys", @@ -2718,9 +2849,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] @@ -2732,25 +2863,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" [[package]] -name = "r2d2" -version = "0.8.9" +name = "radium" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "r2d2_sqlite" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227ab35ff4cbb01fa76da8f062590fe677b93c8d9e8415eb5fa981f2c1dba9d8" -dependencies = [ - "r2d2", - "rusqlite", -] +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "rand" @@ -2768,13 +2884,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha 0.3.0", - "rand_core 0.6.1", + "rand_core 0.6.2", "rand_hc 0.3.0", ] @@ -2795,7 +2911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -2809,9 +2925,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ "getrandom 0.2.2", ] @@ -2831,7 +2947,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -2863,7 +2979,7 @@ checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" dependencies = [ "crossbeam-channel", "crossbeam-deque", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", "lazy_static", "num_cpus", ] @@ -2876,9 +2992,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" dependencies = [ "bitflags", ] @@ -2896,14 +3012,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] @@ -2917,9 +3032,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "remove_dir_all" @@ -2994,21 +3109,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rusqlite" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", -] - [[package]] name = "rust-argon2" version = "0.8.3" @@ -3018,7 +3118,7 @@ dependencies = [ "base64 0.13.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.3", ] [[package]] @@ -3112,15 +3212,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" -dependencies = [ - "parking_lot", -] - [[package]] name = "scopeguard" version = "1.1.0" @@ -3129,9 +3220,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.0.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" dependencies = [ "bitflags", "core-foundation", @@ -3142,9 +3233,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" dependencies = [ "core-foundation-sys", "libc", @@ -3167,9 +3258,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.121" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6159e3c76cab06f6bc466244d43b35e77e9500cd685da87620addadc2a4c40b1" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" dependencies = [ "serde_derive", ] @@ -3186,9 +3277,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.121" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3fcab8778dc651bc65cfab2e4eb64996f3c912b74002fb379c94517e1f27c46" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ "proc-macro2", "quote", @@ -3197,9 +3288,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", @@ -3232,9 +3323,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -3251,9 +3342,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" +checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -3276,9 +3367,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.1.17" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +checksum = "6aa894ef3fade0ee7243422f4fbbd6c2b48e6de767e621d37ef65f2310f53cea" dependencies = [ "libc", "signal-hook-registry", @@ -3342,12 +3433,110 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "sqlformat" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" +dependencies = [ + "lazy_static", + "maplit", + "nom 6.1.2", + "regex", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.1" +source = "git+https://github.com/dignifiedquire/sqlx?branch=fix-pool-time-out#30a6f04a193632f5d43c02da9dc6d0ef84e88920" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.1" +source = "git+https://github.com/dignifiedquire/sqlx?branch=fix-pool-time-out#30a6f04a193632f5d43c02da9dc6d0ef84e88920" +dependencies = [ + "ahash 0.6.3", + "atoi", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-channel", + "crossbeam-queue 0.3.1", + "crossbeam-utils 0.8.3", + "either", + "futures-channel", + "futures-core", + "futures-util", + "hashlink", + "hex", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "url", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.1" +source = "git+https://github.com/dignifiedquire/sqlx?branch=fix-pool-time-out#30a6f04a193632f5d43c02da9dc6d0ef84e88920" +dependencies = [ + "dotenv", + "either", + "futures", + "heck", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.3.0" +source = "git+https://github.com/dignifiedquire/sqlx?branch=fix-pool-time-out#30a6f04a193632f5d43c02da9dc6d0ef84e88920" +dependencies = [ + "async-native-tls", + "async-std", + "native-tls", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3356,11 +3545,11 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "standback" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" dependencies = [ - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -3425,7 +3614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06855fb7c94d3be9b3a57c4d82dfc8a43bb658fbb3b1dda79de89e748d9eb9dd" dependencies = [ "async-std", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", ] [[package]] @@ -3438,6 +3627,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.9.3" @@ -3470,28 +3669,28 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "surf" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7189c787d96fe18fef704950de76d590022d9d70858a4a201e1f07a0666882ea" +checksum = "2a154d33ca6b5e1fe6fd1c760e5a5cc1202425f6cca2e13229f16a69009f6328" dependencies = [ "async-std", "async-trait", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "futures-util", "http-client", "http-types", "log", "mime_guess", - "pin-project-lite 0.1.11", + "pin-project-lite 0.2.6", "serde", "serde_json", ] [[package]] name = "syn" -version = "1.0.59" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cb8b1b4ebf86a89ee88cbd201b022b94138c623644d035185c84d3f41b7e66" +checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" dependencies = [ "proc-macro2", "quote", @@ -3510,6 +3709,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.2.0" @@ -3518,8 +3723,8 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.2", - "redox_syscall 0.2.4", + "rand 0.8.3", + "redox_syscall 0.2.5", "remove_dir_all", "winapi", ] @@ -3544,33 +3749,24 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301bdd13d23c49672926be451130892d274d3ba0b410c18e00daa7990ff38d99" -dependencies = [ - "once_cell", -] - [[package]] name = "time" version = "0.1.44" @@ -3584,16 +3780,16 @@ dependencies = [ [[package]] name = "time" -version = "0.2.24" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273d3ed44dca264b0d6b3665e8d48fb515042d42466fad93d2a45b90ec4058f7" +checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" dependencies = [ "const_fn", "libc", "standback", "stdweb", "time-macros", - "version_check 0.9.2", + "version_check 0.9.3", "winapi", ] @@ -3622,9 +3818,9 @@ dependencies = [ [[package]] name = "tinytemplate" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ada8616fad06a2d0c455adc530de4ef57605a8120cc65da9653e0e9623ca74" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", @@ -3632,9 +3828,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" dependencies = [ "tinyvec_macros", ] @@ -3645,6 +3841,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" +dependencies = [ + "autocfg 1.0.1", + "pin-project-lite 0.2.6", +] + [[package]] name = "toml" version = "0.5.8" @@ -3656,12 +3862,13 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +checksum = "1cad71a0c0d68ab9941d2fb6e82f8fb2e86d9945b94e1661dd0aaea2b88215a9" dependencies = [ "async-trait", "backtrace", + "cfg-if 1.0.0", "enum-as-inner", "futures", "idna", @@ -3675,11 +3882,10 @@ dependencies = [ [[package]] name = "trust-dns-resolver" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb" dependencies = [ - "backtrace", "cfg-if 0.1.10", "futures", "ipconfig", @@ -3714,9 +3920,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicase" @@ -3724,7 +3930,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -3738,9 +3944,9 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ "tinyvec", ] @@ -3763,6 +3969,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.4.0" @@ -3775,9 +3987,9 @@ dependencies = [ [[package]] name = "url" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", "idna", @@ -3802,6 +4014,15 @@ dependencies = [ "serde", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b676010e055c99033117c2343b33a40a30b91fecd6c49055ac9cd2d6c305ab1" +dependencies = [ + "ctor", +] + [[package]] name = "vcpkg" version = "0.2.11" @@ -3810,9 +4031,9 @@ checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" [[package]] name = "vec-arena" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d" +checksum = "34b2f665b594b07095e3ac3f718e13c2197143416fae4c5706cffb7b1af8d7f1" [[package]] name = "version_check" @@ -3822,9 +4043,9 @@ checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "void" @@ -3849,9 +4070,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", "winapi", @@ -3872,9 +4093,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.69" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3882,9 +4103,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.69" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" dependencies = [ "bumpalo", "lazy_static", @@ -3897,9 +4118,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" +checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3909,9 +4130,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.69" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3919,9 +4140,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.69" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" dependencies = [ "proc-macro2", "quote", @@ -3932,15 +4153,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.69" +version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" +checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" [[package]] name = "web-sys" -version = "0.3.46" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" dependencies = [ "js-sys", "wasm-bindgen", @@ -3948,9 +4169,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2bb9fc8309084dd7cd651336673844c1d47f8ef6d2091ec160b27f5c4aa277" +checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c" [[package]] name = "wepoll-sys" @@ -3961,6 +4182,16 @@ dependencies = [ "cc", ] +[[package]] +name = "whoami" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "widestring" version = "0.4.3" @@ -4016,6 +4247,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "x25519-dalek" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index f609d5646..339c2dd50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,6 @@ debug = 0 lto = true [dependencies] -deltachat_derive = { path = "./deltachat_derive" } - libc = "0.2.51" pgp = { version = "0.7.0", default-features = false } hex = "0.4.0" @@ -40,9 +38,6 @@ indexmap = "1.3.0" kamadak-exif = "0.5" once_cell = "1.4.1" regex = "1.1.6" -rusqlite = { version = "0.24", features = ["bundled"] } -r2d2_sqlite = "0.17.0" -r2d2 = "0.8.5" strum = "0.19.0" strum_macros = "0.19.0" backtrace = "0.3.33" @@ -66,6 +61,9 @@ async-std-resolver = "0.19.5" async-tar = "0.3.0" uuid = { version = "0.8", features = ["serde", "v4"] } rust-hsluv = "0.1.4" +sqlx = { git = "https://github.com/dignifiedquire/sqlx", branch = "fix-pool-time-out", features = ["runtime-async-std-native-tls", "sqlite"] } +# keep in sync with sqlx +libsqlite3-sys = { version = "0.20.1", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] } pretty_env_logger = { version = "0.4.0", optional = true } log = {version = "0.4.8", optional = true } @@ -73,6 +71,7 @@ rustyline = { version = "4.1.0", optional = true } ansi_term = { version = "0.12.1", optional = true } dirs = { version = "3.0.1", optional=true } toml = "0.5.6" +num_cpus = "1.13.0" [dev-dependencies] @@ -84,11 +83,11 @@ async-std = { version = "1.6.4", features = ["unstable", "attributes"] } futures-lite = "1.7.0" criterion = "0.3" ansi_term = "0.12.0" +log = "0.4.11" [workspace] members = [ "deltachat-ffi", - "deltachat_derive", ] [[example]] diff --git a/README.md b/README.md index 43a1ca04a..a98749e07 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ $ curl https://sh.rustup.rs -sSf | sh Compile and run Delta Chat Core command line utility, using `cargo`: ``` -$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db +$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db ``` where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist. @@ -95,7 +95,7 @@ $ cargo build -p deltachat_ffi --release - `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed -- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and +- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and SMTP tracing in addition to info messages. ### Expensive tests diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 0c8703457..eb9b4cf38 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -156,7 +156,14 @@ pub unsafe extern "C" fn dc_get_config( } let ctx = &*context; match config::Config::from_str(&to_string_lossy(key)) { - Ok(key) => block_on(async move { ctx.get_config(key).await.unwrap_or_default().strdup() }), + Ok(key) => block_on(async move { + ctx.get_config(key) + .await + .log_err(ctx, "Can't get config") + .unwrap_or_default() + .unwrap_or_default() + .strdup() + }), Err(_) => { warn!(ctx, "dc_get_config(): invalid key"); "".strdup() @@ -225,8 +232,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc: } let ctx = &*context; block_on(async move { - let info = ctx.get_info().await; - render_info(info).unwrap_or_default().strdup() + match ctx.get_info().await { + Ok(info) => render_info(info).unwrap_or_default().strdup(), + Err(err) => { + warn!(ctx, "failed to get info: {}", err); + "".strdup() + } + } }) } @@ -283,7 +295,12 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c } let ctx = &*context; - block_on(async move { ctx.is_configured().await as libc::c_int }) + block_on(async move { + ctx.is_configured() + .await + .log_err(ctx, "failed to get configured state") + .unwrap_or_default() as libc::c_int + }) } #[no_mangle] @@ -768,7 +785,12 @@ pub unsafe extern "C" fn dc_set_draft( Some(&mut ffi_msg.message) }; - block_on(ChatId::new(chat_id).set_draft(&ctx, msg)) + block_on(async move { + ChatId::new(chat_id) + .set_draft(&ctx, msg) + .await + .unwrap_or_log_default(ctx, "failed to set draft"); + }); } #[no_mangle] @@ -863,6 +885,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs( Box::into_raw(Box::new( chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag) .await + .unwrap_or_log_default(ctx, "failed to get chat msgs") .into(), )) }) @@ -876,7 +899,12 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32 } let ctx = &*context; - block_on(async move { ChatId::new(chat_id).get_msg_cnt(&ctx).await as libc::c_int }) + block_on(async move { + ChatId::new(chat_id) + .get_msg_cnt(&ctx) + .await + .unwrap_or_log_default(ctx, "failed to get msg count") as libc::c_int + }) } #[no_mangle] @@ -890,7 +918,12 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt( } let ctx = &*context; - block_on(async move { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await as libc::c_int }) + block_on(async move { + ChatId::new(chat_id) + .get_fresh_msg_cnt(&ctx) + .await + .unwrap_or_log_default(ctx, "failed to get fresh msg cnt") as libc::c_int + }) } #[no_mangle] @@ -988,6 +1021,7 @@ pub unsafe extern "C" fn dc_get_chat_media( or_msg_type3, ) .await + .unwrap_or_log_default(ctx, "Failed get_chat_media") .into(), )) }) @@ -1029,7 +1063,7 @@ pub unsafe extern "C" fn dc_get_next_media( or_msg_type3, ) .await - .map(|msg_id| msg_id.to_u32()) + .map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default()) .unwrap_or(0) }) } @@ -1122,7 +1156,11 @@ pub unsafe extern "C" fn dc_get_chat_contacts( let ctx = &*context; block_on(async move { - let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await); + let arr = dc_array_t::from( + chat::get_chat_contacts(&ctx, ChatId::new(chat_id)) + .await + .unwrap_or_log_default(ctx, "Failed get_chat_contacts"), + ); Box::into_raw(Box::new(arr)) }) } @@ -1148,6 +1186,7 @@ pub unsafe extern "C" fn dc_search_msgs( let arr = dc_array_t::from( ctx.search_msgs(chat_id, to_string_lossy(query)) .await + .unwrap_or_log_default(ctx, "Failed search_msgs") .iter() .map(|msg_id| msg_id.to_u32()) .collect::>(), @@ -1261,7 +1300,8 @@ pub unsafe extern "C" fn dc_set_chat_name( chat_id: u32, name: *const libc::c_char, ) -> libc::c_int { - if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || name.is_null() { + if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null() + { eprintln!("ignoring careless call to dc_set_chat_name()"); return 0; } @@ -1281,7 +1321,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image( chat_id: u32, image: *const libc::c_char, ) -> libc::c_int { - if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 { + if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() { eprintln!("ignoring careless call to dc_set_chat_profile_image()"); return 0; } @@ -1406,7 +1446,12 @@ pub unsafe extern "C" fn dc_get_msg_info( } let ctx = &*context; - block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup() + block_on(async move { + message::get_msg_info(&ctx, MsgId::new(msg_id)) + .await + .unwrap_or_log_default(ctx, "failed to get msg id") + .strdup() + }) } #[no_mangle] @@ -1420,7 +1465,9 @@ pub unsafe extern "C" fn dc_get_msg_html( } let ctx = &*context; - block_on(MsgId::new(msg_id).get_html(&ctx)).strdup() + block_on(MsgId::new(msg_id).get_html(&ctx)) + .unwrap_or_log_default(ctx, "Failed get_msg_html") + .strdup() } #[no_mangle] @@ -1435,10 +1482,13 @@ pub unsafe extern "C" fn dc_get_mime_headers( let ctx = &*context; block_on(async move { - message::get_mime_headers(&ctx, MsgId::new(msg_id)) + let mime = message::get_mime_headers(&ctx, MsgId::new(msg_id)) .await - .map(|s| s.strdup()) - .unwrap_or_else(ptr::null_mut) + .unwrap_or_log_default(ctx, "failed to get mime headers"); + if mime.is_empty() { + return ptr::null_mut(); + } + mime.strdup() }) } @@ -1468,7 +1518,7 @@ pub unsafe extern "C" fn dc_forward_msgs( if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 - || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 + || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() { eprintln!("ignoring careless call to dc_forward_msgs()"); return; @@ -1564,14 +1614,12 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr( } let ctx = &*context; - block_on(Contact::lookup_id_by_addr( - &ctx, - to_string_lossy(addr), - Origin::IncomingReplyTo, - )) - .ok() - .flatten() - .unwrap_or_default() + block_on(async move { + Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo) + .await + .unwrap_or_log_default(ctx, "failed to lookup id") + .unwrap_or(0) + }) } #[no_mangle] @@ -1645,8 +1693,7 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc: block_on(async move { Contact::get_all_blocked(&ctx) .await - .log_err(&ctx, "Can't get blocked count") - .unwrap_or_default() + .unwrap_or_log_default(ctx, "failed to get blocked count") .len() as libc::c_int }) } @@ -1931,7 +1978,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat( chat_id: u32, seconds: libc::c_int, ) { - if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || seconds < 0 { + if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 { eprintln!("ignoring careless call to dc_send_locations_to_chat()"); return; } @@ -2011,7 +2058,8 @@ pub unsafe extern "C" fn dc_get_locations( timestamp_begin as i64, timestamp_end as i64, ) - .await; + .await + .unwrap_or_log_default(ctx, "Failed get_locations"); Box::into_raw(Box::new(dc_array_t::from(res))) }) } @@ -2392,8 +2440,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut block_on(async move { match ffi_chat.chat.get_profile_image(&ctx).await { - Some(p) => p.to_string_lossy().strdup(), - None => ptr::null_mut(), + Ok(Some(p)) => p.to_string_lossy().strdup(), + Ok(None) => ptr::null_mut(), + Err(err) => { + error!(ctx, "failed to get profile image: {:?}", err); + ptr::null_mut() + } } }) } @@ -2407,7 +2459,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 { let ffi_chat = &*chat; let ctx = &*ffi_chat.context; - block_on(ffi_chat.chat.get_color(&ctx)) + block_on(ffi_chat.chat.get_color(&ctx)).unwrap_or_log_default(ctx, "Failed get_color") } #[no_mangle] @@ -3318,6 +3370,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image( .contact .get_profile_image(&ctx) .await + .unwrap_or_log_default(ctx, "failed to get profile image") .map(|p| p.to_string_lossy().strdup()) .unwrap_or_else(std::ptr::null_mut) }) diff --git a/deltachat_derive/Cargo.toml b/deltachat_derive/Cargo.toml deleted file mode 100644 index ee6abfae6..000000000 --- a/deltachat_derive/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "deltachat_derive" -version = "2.0.0" -authors = ["Delta Chat Developers (ML) "] -edition = "2018" -license = "MPL-2.0" - -[lib] -proc-macro = true - -[dependencies] -syn = "1.0.13" -quote = "1.0.2" diff --git a/deltachat_derive/src/lib.rs b/deltachat_derive/src/lib.rs deleted file mode 100644 index 664581464..000000000 --- a/deltachat_derive/src/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![recursion_limit = "128"] -extern crate proc_macro; - -use crate::proc_macro::TokenStream; -use quote::quote; - -// For now, assume (not check) that these macroses are applied to enum without -// data. If this assumption is violated, compiler error will point to -// generated code, which is not very user-friendly. - -#[proc_macro_derive(ToSql)] -pub fn to_sql_derive(input: TokenStream) -> TokenStream { - let ast: syn::DeriveInput = syn::parse(input).unwrap(); - let name = &ast.ident; - - let gen = quote! { - impl rusqlite::types::ToSql for #name { - fn to_sql(&self) -> rusqlite::Result { - let num = *self as i64; - let value = rusqlite::types::Value::Integer(num); - let output = rusqlite::types::ToSqlOutput::Owned(value); - std::result::Result::Ok(output) - } - } - }; - gen.into() -} - -#[proc_macro_derive(FromSql)] -pub fn from_sql_derive(input: TokenStream) -> TokenStream { - let ast: syn::DeriveInput = syn::parse(input).unwrap(); - let name = &ast.ident; - - let gen = quote! { - impl rusqlite::types::FromSql for #name { - fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { - let inner = rusqlite::types::FromSql::column_result(col)?; - Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default()) - } - } - }; - gen.into() -} diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 6b7cea6ba..bdf1ffcb0 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -32,17 +32,13 @@ use std::time::{Duration, SystemTime}; async fn reset_tables(context: &Context, bits: i32) { println!("Resetting tables ({})...", bits); if 0 != bits & 1 { - context - .sql() - .execute("DELETE FROM jobs;", paramsv![]) - .await - .unwrap(); + context.sql().execute("DELETE FROM jobs;").await.unwrap(); println!("(1) Jobs reset."); } if 0 != bits & 2 { context .sql() - .execute("DELETE FROM acpeerstates;", paramsv![]) + .execute("DELETE FROM acpeerstates;") .await .unwrap(); println!("(2) Peerstates reset."); @@ -50,7 +46,7 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 4 { context .sql() - .execute("DELETE FROM keypairs;", paramsv![]) + .execute("DELETE FROM keypairs;") .await .unwrap(); println!("(4) Private keypairs reset."); @@ -58,35 +54,34 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 8 { context .sql() - .execute("DELETE FROM contacts WHERE id>9;", paramsv![]) + .execute("DELETE FROM contacts WHERE id>9;") .await .unwrap(); context .sql() - .execute("DELETE FROM chats WHERE id>9;", paramsv![]) + .execute("DELETE FROM chats WHERE id>9;") .await .unwrap(); context .sql() - .execute("DELETE FROM chats_contacts;", paramsv![]) + .execute("DELETE FROM chats_contacts;") .await .unwrap(); context .sql() - .execute("DELETE FROM msgs WHERE id>9;", paramsv![]) + .execute("DELETE FROM msgs WHERE id>9;") .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![]) + .execute("DELETE FROM leftgrps;") .await .unwrap(); println!("(8) Rest but server config reset."); @@ -120,11 +115,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { real_spec = spec.to_string(); context .sql() - .set_raw_config(context, "import_spec", Some(&real_spec)) + .set_raw_config("import_spec", Some(&real_spec)) .await .unwrap(); } else { - let rs = context.sql().get_raw_config(context, "import_spec").await; + let rs = context.sql().get_raw_config("import_spec").await.unwrap(); if rs.is_none() { error!(context, "Import: No file or folder given."); return false; @@ -201,7 +196,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { contact_id, msgtext.unwrap_or_default(), if msg.has_html() { "[HAS-HTML]️" } else { "" }, - if msg.get_from_id() == 1 as libc::c_uint { + if msg.get_from_id() == 1 { "" } else if msg.get_state() == MessageState::InSeen { "[SEEN]" @@ -292,7 +287,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) { let peerstate = Peerstate::from_addr(context, &addr) .await .expect("peerstate error"); - if peerstate.is_some() && *contact_id != 1 as libc::c_uint { + if peerstate.is_some() && *contact_id != 1 { line2 = format!( ", prefer-encrypt={}", peerstate.as_ref().unwrap().prefer_encrypt @@ -543,7 +538,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu chat_prefix(&chat), chat.get_id(), chat.get_name(), - chat.get_id().get_fresh_msg_cnt(&context).await, + chat.get_id().get_fresh_msg_cnt(&context).await?, if chat.is_muted() { "🔇" } else { "" }, match chat.visibility { ChatVisibility::Normal => "", @@ -605,7 +600,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ensure!(sel_chat.is_some(), "Failed to select chat"); let sel_chat = sel_chat.as_ref().unwrap(); - let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await; + let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?; let msglist: Vec = msglist .into_iter() .map(|x| match x { @@ -615,7 +610,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu }) .collect(); - let members = chat::get_chat_contacts(&context, sel_chat.id).await; + let members = chat::get_chat_contacts(&context, sel_chat.id).await?; let subtitle = if sel_chat.is_device_talk() { "device-talk".to_string() } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { @@ -638,7 +633,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } else { "" }, - match sel_chat.get_profile_image(&context).await { + match sel_chat.get_profile_image(&context).await? { Some(icon) => match icon.to_str() { Some(icon) => format!(" Icon: {}", icon), _ => " Icon: Err".to_string(), @@ -658,14 +653,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu println!( "{} messages.", - sel_chat.get_id().get_msg_cnt(&context).await + sel_chat.get_id().get_msg_cnt(&context).await? ); chat::marknoticed_chat(&context, sel_chat.get_id()).await?; } "createchat" => { ensure!(!arg1.is_empty(), "Argument missing."); - let contact_id: libc::c_int = arg1.parse()?; - let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?; + let contact_id: u32 = arg1.parse()?; + let chat_id = chat::create_by_contact_id(&context, contact_id).await?; println!("Single#{} created successfully.", chat_id,); } @@ -716,11 +711,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ensure!(sel_chat.is_some(), "No chat selected"); ensure!(!arg1.is_empty(), "Argument missing."); - let contact_id_0: libc::c_int = arg1.parse()?; + let contact_id_0: u32 = arg1.parse()?; if chat::add_contact_to_chat( &context, sel_chat.as_ref().unwrap().get_id(), - contact_id_0 as u32, + contact_id_0, ) .await { @@ -732,11 +727,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "removemember" => { ensure!(sel_chat.is_some(), "No chat selected."); ensure!(!arg1.is_empty(), "Argument missing."); - let contact_id_1: libc::c_int = arg1.parse()?; + let contact_id_1: u32 = arg1.parse()?; chat::remove_contact_from_chat( &context, sel_chat.as_ref().unwrap().get_id(), - contact_id_1 as u32, + contact_id_1, ) .await?; @@ -762,7 +757,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ensure!(sel_chat.is_some(), "No chat selected."); let contacts = - chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await; + chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?; println!("Memberlist:"); log_contactlist(&context, &contacts).await; @@ -787,7 +782,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu 0, 0, ) - .await; + .await?; let default_marker = "-".to_string(); for location in &locations { let marker = location.marker.as_ref().unwrap_or(&default_marker); @@ -899,7 +894,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu None }; - let msglist = context.search_msgs(chat, arg1).await; + let msglist = context.search_msgs(chat, arg1).await?; log_msglist(&context, &msglist).await?; println!("{} messages.", msglist.len()); @@ -915,7 +910,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu .unwrap() .get_id() .set_draft(&context, Some(&mut draft)) - .await; + .await?; println!("Draft saved."); } else { sel_chat @@ -923,7 +918,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu .unwrap() .get_id() .set_draft(&context, None) - .await; + .await?; println!("Draft deleted."); } } @@ -946,7 +941,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu Viewtype::Gif, Viewtype::Video, ) - .await; + .await?; println!("{} images or videos: ", images.len()); for (i, data) in images.iter().enumerate() { if 0 == i { @@ -1012,7 +1007,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "msginfo" => { ensure!(!arg1.is_empty(), "Argument missing."); let id = MsgId::new(arg1.parse()?); - let res = message::get_msg_info(&context, id).await; + let res = message::get_msg_info(&context, id).await?; println!("{}", res); } "html" => { @@ -1021,7 +1016,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let file = dirs::home_dir() .unwrap_or_default() .join(format!("msg-{}.html", id.to_u32())); - let html = id.get_html(&context).await.unwrap_or_default(); + let html = id.get_html(&context).await?.unwrap_or_default(); fs::write(&file, html)?; println!("HTML written to: {:#?}", file); } @@ -1081,14 +1076,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "contactinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - let contact_id = arg1.parse()?; + let contact_id: u32 = arg1.parse()?; let contact = Contact::get_by_id(&context, contact_id).await?; let name_n_addr = contact.get_name_n_addr(); let mut res = format!( "Contact info for: {}:\nIcon: {}\n", name_n_addr, - match contact.get_profile_image(&context).await { + match contact.get_profile_image(&context).await? { Some(image) => image.to_str().unwrap().to_string(), None => "NoIcon".to_string(), } @@ -1177,7 +1172,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu // let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t); // println!( // "Sending event {:?}({}), received value {}.", - // event, event as usize, r as libc::c_int, + // event, event as usize, r, // ); // } "fileinfo" => { diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 6f301ead0..ac3b8be50 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -390,7 +390,7 @@ async fn handle_cmd( ctx.configure().await?; } "oauth2" => { - if let Some(addr) = ctx.get_config(config::Config::Addr).await { + if let Some(addr) = ctx.get_config(config::Config::Addr).await? { let oauth2_url = dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await; if oauth2_url.is_none() { diff --git a/src/accounts.rs b/src/accounts.rs index 98950614a..4882e7648 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -135,15 +135,25 @@ impl Accounts { let old_id = self.config.get_selected_account().await; // create new account - let account_config = self.config.new_account(&self.dir).await?; + let account_config = self + .config + .new_account(&self.dir) + .await + .context("failed to create new account")?; let new_dbfile = account_config.dbfile().into(); let new_blobdir = Context::derive_blobdir(&new_dbfile); let res = { - fs::create_dir_all(&account_config.dir).await?; - fs::rename(&dbfile, &new_dbfile).await?; - fs::rename(&blobdir, &new_blobdir).await?; + fs::create_dir_all(&account_config.dir) + .await + .context("failed to create dir")?; + fs::rename(&dbfile, &new_dbfile) + .await + .context("failed to rename dbfile")?; + fs::rename(&blobdir, &new_blobdir) + .await + .context("failed to rename blobdir")?; Ok(()) }; @@ -502,7 +512,10 @@ mod tests { let ctx = accounts.get_selected_account().await; assert_eq!( "me@mail.com", - ctx.get_config(crate::config::Config::Addr).await.unwrap() + ctx.get_config(crate::config::Config::Addr) + .await + .unwrap() + .unwrap() ); } diff --git a/src/blob.rs b/src/blob.rs index 3fdc26091..e52152fc0 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -384,7 +384,7 @@ impl<'a> BlobObject<'a> { let blob_abs = self.to_abs_path(); let img_wh = - match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await) + match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) .unwrap_or_default() { MediaQuality::Balanced => BALANCED_AVATAR_SIZE, @@ -403,7 +403,7 @@ impl<'a> BlobObject<'a> { } let img_wh = - match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await) + match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) .unwrap_or_default() { MediaQuality::Balanced => BALANCED_IMAGE_SIZE, @@ -514,6 +514,10 @@ pub enum BlobError { WrongBlobdir { blobdir: PathBuf, src: PathBuf }, #[error("Blob has a badname {}", .blobname.display())] WrongName { blobname: PathBuf }, + #[error("Sql: {0}")] + Sql(#[from] crate::sql::Error), + #[error("{0}")] + Other(#[from] anyhow::Error), } #[cfg(test)] diff --git a/src/chat.rs b/src/chat.rs index 4f8108e24..aa88ede13 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,16 +1,18 @@ //! # Chat module use std::convert::TryFrom; +use std::convert::TryInto; use std::str::FromStr; use std::time::{Duration, SystemTime}; use anyhow::Context as _; use anyhow::{bail, ensure, format_err, Error}; use async_std::path::{Path, PathBuf}; -use deltachat_derive::{FromSql, ToSql}; +use async_std::prelude::*; use itertools::Itertools; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; +use sqlx::Row; use crate::aheader::EncryptPreference; use crate::blob::{BlobError, BlobObject}; @@ -69,11 +71,10 @@ pub enum ChatItem { Eq, FromPrimitive, ToPrimitive, - FromSql, - ToSql, IntoStaticStr, Serialize, Deserialize, + sqlx::Type, )] #[repr(u32)] pub enum ProtectionStatus { @@ -92,13 +93,25 @@ impl Default for ProtectionStatus { /// Some chat IDs are reserved to identify special chat types. This /// type can represent both the special as well as normal chats. #[derive( - Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord, + Debug, + Copy, + Clone, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + Hash, + PartialOrd, + Ord, + sqlx::Type, )] +#[sqlx(transparent)] pub struct ChatId(u32); impl ChatId { /// Create a new [ChatId]. - pub fn new(id: u32) -> ChatId { + pub const fn new(id: u32) -> ChatId { ChatId(id) } @@ -113,7 +126,7 @@ impl ChatId { /// /// This kind of chat ID can not be used for real chats. pub fn is_special(self) -> bool { - matches!(self.0, 0..=DC_CHAT_ID_LAST_SPECIAL) + (0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0) } /// Chat ID which represents the deaddrop chat. @@ -122,7 +135,7 @@ impl ChatId { /// flagged with [Blocked::Deaddrop]. Usually the UI will show /// these messages as contact requests. pub fn is_deaddrop(self) -> bool { - self.0 == DC_CHAT_ID_DEADDROP + self == DC_CHAT_ID_DEADDROP } /// Chat ID for messages which need to be deleted. @@ -132,7 +145,7 @@ impl ChatId { /// as they are not deleted on the server so that their rfc724_mid /// remains known and downloading them again can be avoided. pub fn is_trash(self) -> bool { - self.0 == DC_CHAT_ID_TRASH + self == DC_CHAT_ID_TRASH } /// Chat ID signifying there are **any** number of archived chats. @@ -142,7 +155,7 @@ impl ChatId { /// /// [`Chatlist`]: crate::chatlist::Chatlist pub fn is_archived_link(self) -> bool { - self.0 == DC_CHAT_ID_ARCHIVED_LINK + self == DC_CHAT_ID_ARCHIVED_LINK } /// Virtual chat ID signalling there are **only** archived chats. @@ -154,7 +167,7 @@ impl ChatId { /// [`DC_GCL_ADD_ALLDONE_HINT`]: crate::constants::DC_GCL_ADD_ALLDONE_HINT /// [`Chatlist`]: crate::chatlist::Chatlist pub fn is_alldone_hint(self) -> bool { - self.0 == DC_CHAT_ID_ALLDONE_HINT + self == DC_CHAT_ID_ALLDONE_HINT } pub async fn set_selfavatar_timestamp( @@ -165,10 +178,13 @@ impl ChatId { context .sql .execute( - "UPDATE contacts + sqlx::query( + "UPDATE contacts SET selfavatar_sent=? WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);", - paramsv![timestamp, self], + ) + .bind(timestamp) + .bind(self), ) .await?; Ok(()) @@ -182,8 +198,9 @@ impl ChatId { context .sql .execute( - "UPDATE chats SET blocked=? WHERE id=?;", - paramsv![new_blocked, self], + sqlx::query("UPDATE chats SET blocked=? WHERE id=?;") + .bind(new_blocked) + .bind(self), ) .await .is_ok() @@ -214,7 +231,7 @@ impl ChatId { match protect { ProtectionStatus::Protected => match chat.typ { Chattype::Single | Chattype::Group => { - let contact_ids = get_chat_contacts(context, self).await; + let contact_ids = get_chat_contacts(context, self).await?; for contact_id in contact_ids.into_iter() { let contact = Contact::get_by_id(context, contact_id).await?; if contact.is_verified(context).await != VerifiedStatus::BidirectVerified { @@ -231,8 +248,9 @@ impl ChatId { context .sql .execute( - "UPDATE chats SET protected=? WHERE id=?;", - paramsv![protect, self], + sqlx::query("UPDATE chats SET protected=? WHERE id=?;") + .bind(protect) + .bind(self), ) .await?; @@ -315,8 +333,10 @@ impl ChatId { context .sql .execute( - "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;", - paramsv![MessageState::InNoticed, self, MessageState::InFresh], + sqlx::query("UPDATE msgs SET state=? WHERE chat_id=? AND state=?;") + .bind(MessageState::InNoticed) + .bind(self) + .bind(MessageState::InFresh), ) .await?; } @@ -324,8 +344,9 @@ impl ChatId { context .sql .execute( - "UPDATE chats SET archived=? WHERE id=?;", - paramsv![visibility, self], + sqlx::query("UPDATE chats SET archived=? WHERE id=?;") + .bind(visibility) + .bind(self), ) .await?; @@ -343,8 +364,7 @@ impl ChatId { context .sql .execute( - "UPDATE chats SET archived=0 WHERE id=? and archived=1", - paramsv![self], + sqlx::query("UPDATE chats SET archived=0 WHERE id=? and archived=1").bind(self), ) .await?; Ok(()) @@ -363,27 +383,26 @@ impl ChatId { context .sql .execute( - "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", - paramsv![self], + sqlx::query( + "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", + ) + .bind(self), ) .await?; context .sql - .execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self]) + .execute(sqlx::query("DELETE FROM msgs WHERE chat_id=?;").bind(self)) .await?; context .sql - .execute( - "DELETE FROM chats_contacts WHERE chat_id=?;", - paramsv![self], - ) + .execute(sqlx::query("DELETE FROM chats_contacts WHERE chat_id=?;").bind(self)) .await?; context .sql - .execute("DELETE FROM chats WHERE id=?;", paramsv![self]) + .execute(sqlx::query("DELETE FROM chats WHERE id=?;").bind(self)) .await?; context.emit_event(EventType::MsgsChanged { @@ -407,14 +426,18 @@ impl ChatId { /// Sets draft message. /// /// Passing `None` as message just deletes the draft - pub async fn set_draft(self, context: &Context, msg: Option<&mut Message>) { + pub async fn set_draft( + self, + context: &Context, + msg: Option<&mut Message>, + ) -> Result<(), Error> { if self.is_special() { - return; + return Ok(()); } let changed = match msg { - None => self.maybe_delete_draft(context).await, - Some(msg) => self.set_draft_raw(context, msg).await, + None => self.maybe_delete_draft(context).await?, + Some(msg) => self.set_draft_raw(context, msg).await?, }; if changed { @@ -423,33 +446,36 @@ impl ChatId { msg_id: MsgId::new(0), }); } + + Ok(()) } - // similar to as dc_set_draft() but does not emit an event - async fn set_draft_raw(self, context: &Context, msg: &mut Message) -> bool { - let deleted = self.maybe_delete_draft(context).await; + /// Similar to as dc_set_draft() but does not emit an event + async fn set_draft_raw(self, context: &Context, msg: &mut Message) -> Result { + 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 + Ok(deleted || set) } - async fn get_draft_msg_id(self, context: &Context) -> Option { + async fn get_draft_msg_id(self, context: &Context) -> Result, Error> { context .sql - .query_get_value::( - context, - "SELECT id FROM msgs WHERE chat_id=? AND state=?;", - paramsv![self, MessageState::OutDraft], + .query_get_value::<_, MsgId>( + sqlx::query("SELECT id FROM msgs WHERE chat_id=? AND state=?;") + .bind(self) + .bind(MessageState::OutDraft), ) .await + .map_err(Into::into) } pub async fn get_draft(self, context: &Context) -> Result, Error> { if self.is_special() { return Ok(None); } - match self.get_draft_msg_id(context).await { + 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)) @@ -461,10 +487,10 @@ impl ChatId { /// Delete draft message in specified chat, if there is one. /// /// Returns `true`, if message was deleted, `false` otherwise. - 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, + async fn maybe_delete_draft(self, context: &Context) -> Result { + match self.get_draft_msg_id(context).await? { + Some(msg_id) => Ok(msg_id.delete_from_db(context).await.is_ok()), + None => Ok(false), } } @@ -497,38 +523,43 @@ impl ChatId { context .sql .execute( - "INSERT INTO msgs (chat_id, from_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to) - VALUES (?,?,?, ?,?,?,?,?,?);", - paramsv![ - self, - DC_CONTACT_ID_SELF, - time(), - msg.viewtype, - MessageState::OutDraft, - msg.text.as_deref().unwrap_or(""), - msg.param.to_string(), - 1, - msg.in_reply_to.as_deref().unwrap_or_default(), - ], + sqlx::query( + "INSERT INTO msgs ( + chat_id, + from_id, + timestamp, + type, + state, + txt, + param, + hidden, + mime_in_reply_to) + VALUES (?,?,?,?,?,?,?,?,?);", + ) + .bind(self) + .bind(DC_CONTACT_ID_SELF as i32) + .bind(time()) + .bind(msg.viewtype) + .bind(MessageState::OutDraft) + .bind(msg.text.as_deref().unwrap_or("")) + .bind(msg.param.to_string()) + .bind(1i32) + .bind(msg.in_reply_to.as_deref().unwrap_or_default()), ) .await?; Ok(()) } /// Returns number of messages in a chat. - pub async fn get_msg_cnt(self, context: &Context) -> usize { - context + pub async fn get_msg_cnt(self, context: &Context) -> Result { + let count = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", - paramsv![self], - ) - .await - .unwrap_or_default() as usize + .count(sqlx::query("SELECT COUNT(*) FROM msgs WHERE chat_id=?;").bind(self)) + .await?; + Ok(count as usize) } - pub async fn get_fresh_msg_cnt(self, context: &Context) -> usize { + pub async fn get_fresh_msg_cnt(self, context: &Context) -> Result { // this function is typically used to show a badge counter beside _each_ chatlist item. // to make this as fast as possible, esp. on older devices, we added an combined index over the rows used for querying. // so if you alter the query here, you may want to alter the index over `(state, hidden, chat_id)` in `sql.rs`. @@ -539,25 +570,26 @@ impl ChatId { // the times are average, no matter if there are fresh messages or not - // and have to be multiplied by the number of items shown at once on the chatlist, // so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :) - context + let count = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) + .count( + sqlx::query( + "SELECT COUNT(*) FROM msgs WHERE state=10 AND hidden=0 AND chat_id=?;", - paramsv![self], + ) + .bind(self), ) - .await - .unwrap_or_default() as usize + .await?; + Ok(count as usize) } 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=?", paramsv![self]) + .query_get_value(sqlx::query("SELECT param FROM chats WHERE id=?").bind(self)) .await?; Ok(res .map(|s| s.parse().unwrap_or_default()) @@ -574,65 +606,58 @@ impl ChatId { Ok(self.get_param(context).await?.exists(Param::Devicetalk)) } - async fn parent_query( + async fn parent_query( self, context: &Context, fields: &str, - f: F, - ) -> sql::Result> - where - F: FnOnce(&rusqlite::Row) -> rusqlite::Result, - { - let sql = &context.sql; - let query = format!( + ) -> sql::Result> { + let q = format!( "SELECT {} \ FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \ ORDER BY timestamp DESC, id DESC \ LIMIT 1;", fields ); - sql.query_row_optional( - query, - paramsv![ - self, - MessageState::OutPreparing, - MessageState::OutDraft, - MessageState::OutPending, - MessageState::OutFailed - ], - f, - ) - .await + let query = sqlx::query(&q) + .bind(self) + .bind(MessageState::OutPreparing) + .bind(MessageState::OutDraft) + .bind(MessageState::OutPending) + .bind(MessageState::OutFailed); + + let row = context.sql.fetch_optional(query).await?; + Ok(row) } - 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)?, row.get(3)?)); - let (rfc724_mid, mime_in_reply_to, mime_references, error): ( - String, - String, - String, - String, - ) = self + async fn get_parent_mime_headers( + self, + context: &Context, + ) -> sql::Result> { + if let Some(row) = self .parent_query( context, "rfc724_mid, mime_in_reply_to, mime_references, error", - collect, ) - .await - .ok() - .flatten()?; + .await? + { + let rfc724_mid: String = row.try_get(0)?; + let mime_in_reply_to: String = row.try_get(1)?; + let mime_references: String = row.try_get(2)?; + let error: String = row.try_get(3)?; - if !error.is_empty() { - // Do not reply to error messages. - // - // An error message could be a group chat message that we failed to decrypt and - // assigned to 1:1 chat. A reply to it will show up as a reply to group message - // on the other side. To avoid such situations, it is better not to reply to - // error messages at all. - None + if !error.is_empty() { + // Do not reply to error messages. + // + // An error message could be a group chat message that we failed to decrypt and + // assigned to 1:1 chat. A reply to it will show up as a reply to group message + // on the other side. To avoid such situations, it is better not to reply to + // error messages at all. + Ok(None) + } else { + Ok(Some((rfc724_mid, mime_in_reply_to, mime_references))) + } } else { - Some((rfc724_mid, mime_in_reply_to, mime_references)) + Ok(None) } } @@ -647,7 +672,7 @@ impl ChatId { let mut ret = String::new(); for contact_id in get_chat_contacts(context, self) - .await + .await? .iter() .filter(|&contact_id| *contact_id > DC_CONTACT_ID_LAST_SPECIAL) { @@ -704,31 +729,6 @@ impl std::fmt::Display for ChatId { } } -/// Allow converting [ChatId] to an SQLite type. -/// -/// This allows you to directly store [ChatId] into the database as -/// well as query for a [ChatId]. -impl rusqlite::types::ToSql for ChatId { - fn to_sql(&self) -> rusqlite::Result { - let val = rusqlite::types::Value::Integer(self.0 as i64); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - -/// Allow converting an SQLite integer directly into [ChatId]. -impl rusqlite::types::FromSql for ChatId { - fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { - i64::column_result(value).and_then(|val| { - if 0 <= val && val <= std::u32::MAX as i64 { - Ok(ChatId::new(val as u32)) - } else { - Err(rusqlite::types::FromSqlError::OutOfRange(val)) - } - }) - } -} - /// An object representing a single chat in memory. /// Chat objects are created using eg. `Chat::load_from_db` /// and are not updated on database changes; @@ -750,70 +750,63 @@ pub struct Chat { impl Chat { /// Loads chat from the database by its ID. pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result { - let res = context + let row = context .sql - .query_row( - "SELECT c.type, c.name, c.grpid, c.param, c.archived, + .fetch_one( + sqlx::query( + "SELECT c.type, c.name, c.grpid, c.param, c.archived, c.blocked, c.locations_send_until, c.muted_until, c.protected FROM chats c WHERE c.id=?;", - 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)?, - protected: row.get(8)?, - }; - Ok(c) - }, + ) + .bind(chat_id), ) - .await; + .await?; - match res { - Err(err @ crate::sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { - Err(err.into()) - } - Err(err) => { - error!( - context, - "chat: failed to load from db {}: {:?}", chat_id, err - ); - Err(err.into()) - } - Ok(mut chat) => { - if chat.id.is_deaddrop() { - chat.name = stock_str::dead_drop(context).await; - } else if chat.id.is_archived_link() { - let tempname = stock_str::archived_chats(context).await; - let cnt = dc_get_archived_cnt(context).await; - chat.name = format!("{} ({})", tempname, cnt); - } else { - if chat.typ == Chattype::Single { - let contacts = get_chat_contacts(context, chat.id).await; - let mut chat_name = "Err [Name not found]".to_owned(); + let mut chat = Chat { + id: chat_id, + typ: row.try_get(0)?, + name: row.try_get::(1)?, + grpid: row.try_get::(2)?, + param: row.try_get::(3)?.parse().unwrap_or_default(), + visibility: row.try_get(4)?, + blocked: row.try_get::, _>(5)?.unwrap_or_default(), + is_sending_locations: row.try_get(6)?, + mute_duration: row.try_get(7)?, + protected: row.try_get(8)?, + }; + + if chat.id.is_deaddrop() { + chat.name = stock_str::dead_drop(context).await; + } else if chat.id.is_archived_link() { + let tempname = stock_str::archived_chats(context).await; + let cnt = dc_get_archived_cnt(context).await?; + chat.name = format!("{} ({})", tempname, cnt); + } else { + if chat.typ == Chattype::Single { + let mut chat_name = "Err [Name not found]".to_owned(); + match get_chat_contacts(context, chat.id).await { + Ok(contacts) => { if let Some(contact_id) = contacts.first() { 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 = stock_str::saved_messages(context).await; - } else if chat.param.exists(Param::Devicetalk) { - chat.name = stock_str::device_messages(context).await; + Err(err) => { + error!(context, "faild to load contacts for {}: {:?}", chat.id, err); } } - Ok(chat) + chat.name = chat_name; + } + if chat.param.exists(Param::Selftalk) { + chat.name = stock_str::saved_messages(context).await; + } else if chat.param.exists(Param::Devicetalk) { + chat.name = stock_str::device_messages(context).await; } } + + Ok(chat) } pub fn is_self_talk(&self) -> bool { @@ -838,8 +831,9 @@ impl Chat { context .sql .execute( - "UPDATE chats SET param=? WHERE id=?", - paramsv![self.param.to_string(), self.id], + sqlx::query("UPDATE chats SET param=? WHERE id=?") + .bind(self.param.to_string()) + .bind(self.id), ) .await?; Ok(()) @@ -860,13 +854,13 @@ impl Chat { &self.name } - pub async fn get_profile_image(&self, context: &Context) -> Option { + pub async fn get_profile_image(&self, context: &Context) -> Result, Error> { if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { - return Some(dc_get_abs_path(context, image_rel)); + return Ok(Some(dc_get_abs_path(context, image_rel))); } } else if self.typ == Chattype::Single { - let contacts = get_chat_contacts(context, self.id).await; + 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).await { return contact.get_profile_image(context).await; @@ -874,18 +868,18 @@ impl Chat { } } - None + Ok(None) } - pub async fn get_gossiped_timestamp(&self, context: &Context) -> i64 { + pub async fn get_gossiped_timestamp(&self, context: &Context) -> Result { get_gossiped_timestamp(context, self.id).await } - pub async fn get_color(&self, context: &Context) -> u32 { + pub async fn get_color(&self, context: &Context) -> Result { let mut color = 0; if self.typ == Chattype::Single { - let contacts = get_chat_contacts(context, self.id).await; + 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).await { color = contact.get_color(); @@ -895,7 +889,7 @@ impl Chat { color = str_to_color(&self.name); } - color + Ok(color) } /// Returns a struct describing the current state of the chat. @@ -913,12 +907,12 @@ impl Chat { name: self.name.clone(), archived: self.visibility == ChatVisibility::Archived, param: self.param.to_string(), - gossiped_timestamp: self.get_gossiped_timestamp(context).await, + gossiped_timestamp: self.get_gossiped_timestamp(context).await?, is_sending_locations: self.is_sending_locations, - color: self.get_color(context).await, + color: self.get_color(context).await?, profile_image: self .get_profile_image(context) - .await + .await? .map(Into::into) .unwrap_or_else(std::path::PathBuf::new), draft, @@ -985,7 +979,7 @@ impl Chat { let from = context .get_config(Config::ConfiguredAddr) - .await + .await? .context("Cannot prepare message for sending, address is not configured.")?; let new_rfc724_mid = { @@ -1000,11 +994,10 @@ impl Chat { if let Some(id) = context .sql .query_get_value( - context, - "SELECT contact_id FROM chats_contacts WHERE chat_id=?;", - paramsv![self.id], + sqlx::query("SELECT contact_id FROM chats_contacts WHERE chat_id=?;") + .bind(self.id), ) - .await + .await? { to_id = id; } else { @@ -1033,7 +1026,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).await + self.id.get_parent_mime_headers(context).await? { // "In-Reply-To:" is not changed if it is set manually. // This does not affect "References:" header, it will contain "default parent" (the @@ -1077,16 +1070,16 @@ impl Chat { && context .sql .execute( - "INSERT INTO locations \ + sqlx::query( + "INSERT INTO locations \ (timestamp,from_id,chat_id, latitude,longitude,independent)\ - VALUES (?,?,?, ?,?,1);", // 1=DC_CONTACT_ID_SELF - 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(), - ], + VALUES (?,?,?, ?,?,1);", + ) + .bind(timestamp) + .bind(DC_CONTACT_ID_SELF as i32) + .bind(self.id) + .bind(msg.param.get_float(Param::SetLatitude).unwrap_or_default()) + .bind(msg.param.get_float(Param::SetLongitude).unwrap_or_default()), ) .await .is_ok() @@ -1094,12 +1087,11 @@ impl Chat { location_id = context .sql .get_rowid2( - context, "locations", "timestamp", timestamp, "from_id", - DC_CONTACT_ID_SELF as i32, + DC_CONTACT_ID_SELF as i64, ) .await?; } @@ -1116,7 +1108,7 @@ impl Chat { let new_mime_headers = if msg.has_html() { let html = if msg.param.exists(Param::Forwarded) { - msg.get_id().get_html(context).await + msg.get_id().get_html(context).await? } else { msg.param.get(Param::SendHtml).map(|s| s.to_string()) }; @@ -1134,7 +1126,8 @@ impl Chat { if context .sql .execute( - "INSERT INTO msgs ( + sqlx::query( + "INSERT INTO msgs ( rfc724_mid, chat_id, from_id, @@ -1153,34 +1146,34 @@ impl Chat { location_id, ephemeral_timer, ephemeral_timestamp) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", - paramsv![ - new_rfc724_mid, - self.id, - DC_CONTACT_ID_SELF, - to_id as i32, - timestamp, - msg.viewtype, - msg.state, - msg.text.as_ref().cloned().unwrap_or_default(), - &msg.subject, - msg.param.to_string(), - msg.hidden, - msg.in_reply_to.as_deref().unwrap_or_default(), - new_references, - new_mime_headers.is_some(), - new_mime_headers, - location_id as i32, - ephemeral_timer, - ephemeral_timestamp - ], + ) + .bind(&new_rfc724_mid) + .bind(self.id) + .bind(DC_CONTACT_ID_SELF as i32) + .bind(to_id as i32) + .bind(timestamp) + .bind(msg.viewtype) + .bind(msg.state) + .bind(msg.text.as_ref().cloned().unwrap_or_default()) + .bind(&msg.subject) + .bind(msg.param.to_string()) + .bind(msg.hidden) + .bind(msg.in_reply_to.as_deref().unwrap_or_default()) + .bind(new_references) + .bind(new_mime_headers.is_some()) + .bind(new_mime_headers.unwrap_or_default()) + .bind(location_id as i32) + .bind(ephemeral_timer) + .bind(ephemeral_timestamp), ) .await .is_ok() { msg_id = context .sql - .get_rowid(context, "msgs", "rfc724_mid", new_rfc724_mid) + .get_rowid("msgs", "rfc724_mid", new_rfc724_mid) .await?; } else { error!( @@ -1190,7 +1183,7 @@ impl Chat { } schedule_ephemeral_task(context).await; - Ok(MsgId::new(msg_id)) + Ok(MsgId::new(u32::try_from(msg_id)?)) } } @@ -1201,30 +1194,53 @@ pub enum ChatVisibility { Pinned, } -impl rusqlite::types::ToSql for ChatVisibility { - fn to_sql(&self) -> rusqlite::Result { - let visibility = match &self { +impl ChatVisibility { + fn to_u32(self) -> u32 { + match self { ChatVisibility::Normal => 0, ChatVisibility::Archived => 1, ChatVisibility::Pinned => 2, - }; - let val = rusqlite::types::Value::Integer(visibility); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) + } + } + + fn from_u32(val: u32) -> Self { + match val { + 2 => ChatVisibility::Pinned, + 1 => ChatVisibility::Archived, + 0 => ChatVisibility::Normal, + // fallback to to Normal for unknown values, may happen eg. on imports created by a newer version. + _ => ChatVisibility::Normal, + } } } -impl rusqlite::types::FromSql for ChatVisibility { - fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { - i64::column_result(value).map(|val| { - match val { - 2 => ChatVisibility::Pinned, - 1 => ChatVisibility::Archived, - 0 => ChatVisibility::Normal, - // fallback to to Normal for unknown values, may happen eg. on imports created by a newer version. - _ => ChatVisibility::Normal, - } - }) +impl sqlx::Type for ChatVisibility { + fn type_info() -> sqlx::sqlite::SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} + +impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for ChatVisibility { + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> sqlx::encode::IsNull { + args.push(sqlx::sqlite::SqliteArgumentValue::Int64( + self.to_u32() as i64 + )); + + sqlx::encode::IsNull::No + } +} + +impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for ChatVisibility { + fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let value: u32 = sqlx::Decode::decode(value)?; + Ok(ChatVisibility::from_u32(value)) } } @@ -1425,8 +1441,10 @@ async fn update_special_chat_name( context .sql .execute( - "UPDATE chats SET name=? WHERE id=? AND name!=?;", - paramsv![name, chat_id, name], + sqlx::query("UPDATE chats SET name=? WHERE id=? AND name!=?;") + .bind(&name) + .bind(chat_id) + .bind(&name), ) .await?; } @@ -1467,31 +1485,40 @@ pub(crate) async fn create_or_lookup_by_contact_id( context .sql - .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(), - ], - )?; + .transaction(move |conn| { + Box::pin(async move { + sqlx::query( + "INSERT INTO chats ( + type, + name, + param, + blocked, + created_timestamp + ) + VALUES(?, ?, ?, ?, ?)", + ) + .bind(Chattype::Single) + .bind(chat_name) + .bind(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(), + }) + .bind(create_blocked) + .bind(time()) + .execute(&mut *conn) + .await?; - tx.execute( - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)", - params![contact_id], - )?; - - tx.commit()?; - Ok(()) + sqlx::query( + "INSERT INTO chats_contacts + (chat_id, contact_id) + VALUES((SELECT last_insert_rowid()), ?)", + ) + .bind(contact_id) + .execute(&mut *conn) + .await?; + Ok(()) + }) }) .await?; @@ -1510,26 +1537,25 @@ pub(crate) async fn lookup_by_contact_id( ) -> Result<(ChatId, Blocked), Error> { ensure!(context.sql.is_open().await, "Database not available"); - context + let row = context .sql - .query_row( - "SELECT c.id, c.blocked + .fetch_one( + sqlx::query( + "SELECT c.id, c.blocked FROM chats c INNER JOIN chats_contacts j ON c.id=j.chat_id WHERE c.type=100 AND c.id>9 AND j.contact_id=?;", - paramsv![contact_id as i32], - |row| { - Ok(( - row.get::<_, ChatId>(0)?, - row.get::<_, Option<_>>(1)?.unwrap_or_default(), - )) - }, + ) + .bind(contact_id), ) - .await - .map_err(Into::into) + .await?; + Ok(( + row.try_get::(0)?, + row.try_get::, _>(1)?.unwrap_or_default(), + )) } pub async fn get_by_contact_id(context: &Context, contact_id: u32) -> Result { @@ -1663,8 +1689,9 @@ pub async fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: context .sql .exists( - "SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id=?;", - paramsv![chat_id, contact_id as i32], + sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;") + .bind(chat_id) + .bind(contact_id), ) .await .unwrap_or_default() @@ -1813,7 +1840,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re chat_id ); - let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await { + let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await? { if !instance.is_empty() { instance } else { @@ -1839,7 +1866,7 @@ pub async fn get_chat_msgs( chat_id: ChatId, flags: u32, marker1before: Option, -) -> Vec { +) -> Result, Error> { match delete_expired_messages(context).await { Err(err) => warn!(context, "Failed to delete expired messages: {}", err), Ok(messages_deleted) => { @@ -1857,70 +1884,11 @@ pub async fn get_chat_msgs( } } - let process_row = if (flags & DC_GCM_INFO_ONLY) != 0 { - |row: &rusqlite::Row| { - // is_info logic taken from Message.is_info() - let params = row.get::<_, String>("param")?; - let (from_id, to_id) = (row.get::<_, u32>("from_id")?, row.get::<_, u32>("to_id")?); - let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO as u32 - || to_id == DC_CONTACT_ID_INFO as u32 - || match Params::from_str(¶ms) { - Ok(p) => { - let cmd = p.get_cmd(); - cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage - } - _ => false, - }; - - Ok(( - row.get::<_, MsgId>("id")?, - row.get::<_, i64>("timestamp")?, - !is_info_msg, - )) - } - } else { - |row: &rusqlite::Row| { - Ok(( - row.get::<_, MsgId>("id")?, - row.get::<_, i64>("timestamp")?, - false, - )) - } - }; - let process_rows = |rows: rusqlite::MappedRows<_>| { - let mut ret = Vec::new(); - let mut last_day = 0; - let cnv_to_local = dc_gm2local_offset(); - for row in rows { - let (curr_id, ts, exclude_message): (MsgId, i64, bool) = row?; - if let Some(marker_id) = marker1before { - if curr_id == marker_id { - ret.push(ChatItem::Marker1); - } - } - if (flags & DC_GCM_ADDDAYMARKER) != 0 { - let curr_local_timestamp = ts + cnv_to_local; - let curr_day = curr_local_timestamp / 86400; - if curr_day != last_day { - ret.push(ChatItem::DayMarker { - timestamp: curr_day, - }); - last_day = curr_day; - } - } - if !exclude_message { - ret.push(ChatItem::Message { msg_id: curr_id }); - } - } - Ok(ret) - }; - let success = if chat_id.is_deaddrop() { - let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await) + let query = if chat_id.is_deaddrop() { + 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 + sqlx::query( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN chats ON m.chat_id=chats.id @@ -1933,16 +1901,15 @@ pub async fn get_chat_msgs( AND contacts.blocked=0 AND m.msgrmsg>=? ORDER BY m.timestamp,m.id;", - paramsv![if show_emails == ShowEmails::All { 0 } else { 1 }], - process_row, - process_rows, - ) - .await + ) + .bind(if show_emails == ShowEmails::All { + 0i32 + } else { + 1i32 + }) } else if (flags & DC_GCM_INFO_ONLY) != 0 { - context - .sql - .query_map( - // GLOB is used here instead of LIKE becase it is case-sensitive + sqlx::query( + // GLOB is used here instead of LIKE becase it is case-sensitive "SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id FROM msgs m WHERE m.chat_id=? @@ -1952,34 +1919,69 @@ pub async fn get_chat_msgs( OR m.from_id == ? OR m.to_id == ? ) - ORDER BY m.timestamp, m.id;", - paramsv![chat_id, DC_CONTACT_ID_INFO, DC_CONTACT_ID_INFO], - process_row, - process_rows, - ) - .await + ORDER BY m.timestamp, m.id;" + ).bind(chat_id) + .bind(DC_CONTACT_ID_INFO) + .bind(DC_CONTACT_ID_INFO) } else { - context - .sql - .query_map( - "SELECT m.id AS id, m.timestamp AS timestamp + sqlx::query( + "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;", - paramsv![chat_id], - process_row, - process_rows, - ) - .await + ) + .bind(chat_id) }; - match success { - Ok(ret) => ret, - Err(e) => { - error!(context, "Failed to get chat messages: {}", e); - Vec::new() + + let mut rows = context.sql.fetch(query).await?; + + let mut ret = Vec::new(); + let mut last_day = 0; + let cnv_to_local = dc_gm2local_offset(); + + while let Some(row) = rows.next().await { + let row = row?; + if (flags & DC_GCM_INFO_ONLY) != 0 { + // is_info logic taken from Message.is_info() + let params = row.try_get::("param")?; + let from_id = row.try_get::("from_id")?; + let to_id = row.try_get::("to_id")?; + let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO + || to_id == DC_CONTACT_ID_INFO + || match Params::from_str(¶ms) { + Ok(p) => { + let cmd = p.get_cmd(); + cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage + } + _ => false, + }; + + if !is_info_msg { + continue; + } } + + let curr_id = row.try_get::("id")?; + let ts = row.try_get::("timestamp")?; + if let Some(marker_id) = marker1before { + if curr_id == marker_id { + ret.push(ChatItem::Marker1); + } + } + if (flags & DC_GCM_ADDDAYMARKER) != 0 { + let curr_local_timestamp = ts + cnv_to_local; + let curr_day = curr_local_timestamp / 86400; + if curr_day != last_day { + ret.push(ChatItem::DayMarker { + timestamp: curr_day, + }); + last_day = curr_day; + } + } + ret.push(ChatItem::Message { msg_id: curr_id }); } + Ok(ret) } pub(crate) async fn marknoticed_chat_if_older_than( @@ -1990,11 +1992,9 @@ pub(crate) async fn marknoticed_chat_if_older_than( if let Some(chat_timestamp) = context .sql .query_get_value( - context, - "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", - paramsv![chat_id], + sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?").bind(chat_id), ) - .await + .await? { if timestamp > chat_timestamp { marknoticed_chat(context, chat_id).await?; @@ -2005,26 +2005,31 @@ pub(crate) async fn marknoticed_chat_if_older_than( pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> { // "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning - if !context + let exists = context .sql .exists( - "SELECT id FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, chat_id], + sqlx::query("SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;") + .bind(MessageState::InFresh) + .bind(chat_id), ) - .await? - { + .await?; + if !exists { return Ok(()); } context .sql .execute( - "UPDATE msgs + sqlx::query( + "UPDATE msgs SET state=? WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], + ) + .bind(MessageState::InNoticed) + .bind(MessageState::InFresh) + .bind(chat_id), ) .await?; @@ -2039,43 +2044,38 @@ pub async fn get_chat_media( msg_type: Viewtype, msg_type2: Viewtype, msg_type3: Viewtype, -) -> Vec { +) -> Result, Error> { // TODO This query could/should be converted to `AND type IN (?, ?, ?)`. - context + let list = context .sql - .query_map( - "SELECT id + .fetch( + sqlx::query( + "SELECT id FROM msgs WHERE chat_id=? AND (type=? OR type=? OR type=?) ORDER BY timestamp, id;", - paramsv![ - chat_id, - msg_type, - if msg_type2 != Viewtype::Unknown { - msg_type2 - } else { - msg_type - }, - if msg_type3 != Viewtype::Unknown { - msg_type3 - } else { - msg_type - }, - ], - |row| row.get::<_, MsgId>(0), - |ids| { - let mut ret = Vec::new(); - for id in ids { - if let Ok(msg_id) = id { - ret.push(msg_id) - } - } - Ok(ret) - }, + ) + .bind(chat_id) + .bind(msg_type) + .bind(if msg_type2 != Viewtype::Unknown { + msg_type2 + } else { + msg_type + }) + .bind(if msg_type3 != Viewtype::Unknown { + msg_type3 + } else { + msg_type + }), ) - .await - .unwrap_or_default() + .await? + .map(|row| row?.try_get(0)) + .filter_map(|row| row.ok()) + .collect() + .await; + + Ok(list) } /// Indicates the direction over which to iterate. @@ -2093,7 +2093,7 @@ pub async fn get_next_media( msg_type: Viewtype, msg_type2: Viewtype, msg_type3: Viewtype, -) -> Option { +) -> Result, Error> { let mut ret: Option = None; if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await { @@ -2108,7 +2108,7 @@ pub async fn get_next_media( msg_type2, msg_type3, ) - .await; + .await?; for (i, msg_id) in list.iter().enumerate() { if curr_msg_id == *msg_id { match direction { @@ -2127,35 +2127,39 @@ pub async fn get_next_media( } } } - ret + Ok(ret) } -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) */ +pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result, Error> { + // 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) if chat_id.is_deaddrop() { - return Vec::new(); + return Ok(Vec::new()); } // we could also create a list for all contacts in the deaddrop by searching contacts belonging to chats with // chats.blocked=2, however, currently this is not needed - context + let list = context .sql - .query_map( - "SELECT cc.contact_id + .fetch( + sqlx::query( + "SELECT cc.contact_id FROM chats_contacts cc LEFT JOIN contacts c ON c.id=cc.contact_id WHERE cc.chat_id=? ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;", - paramsv![chat_id], - |row| row.get::<_, u32>(0), - |ids| ids.collect::, _>>().map_err(Into::into), + ) + .bind(chat_id), ) - .await - .unwrap_or_default() + .await? + .map(|row| row?.try_get(0)) + .collect::>() + .await?; + + Ok(list) } pub async fn create_group_chat( @@ -2169,26 +2173,28 @@ pub async fn create_group_chat( let draft_txt = stock_str::new_group_draft(context, &chat_name).await; let grpid = dc_create_id(); - context.sql.execute( - "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - paramsv![ - Chattype::Group, - chat_name, - grpid, - time(), - ], - ).await?; - - let row_id = context + context .sql - .get_rowid(context, "chats", "grpid", grpid) + .execute( + sqlx::query( + "INSERT INTO chats + (type, name, grpid, param, created_timestamp) + VALUES(?, ?, ?, \'U=1\', ?);", + ) + .bind(Chattype::Group) + .bind(chat_name) + .bind(&grpid) + .bind(time()), + ) .await?; - let chat_id = ChatId::new(row_id); + let row_id = context.sql.get_rowid("chats", "grpid", grpid).await?; + + let chat_id = ChatId::new(u32::try_from(row_id)?); 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).await; + chat_id.set_draft_raw(context, &mut draft_msg).await?; } context.emit_event(EventType::MsgsChanged { @@ -2214,8 +2220,9 @@ pub(crate) async fn add_to_chat_contacts_table( match context .sql .execute( - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", - paramsv![chat_id, contact_id as i32], + sqlx::query("INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)") + .bind(chat_id) + .bind(contact_id as i32), ) .await { @@ -2240,8 +2247,9 @@ pub(crate) async fn remove_from_chat_contacts_table( match context .sql .execute( - "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", - paramsv![chat_id, contact_id as i32], + sqlx::query("DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?") + .bind(chat_id) + .bind(contact_id as i32), ) .await { @@ -2294,7 +2302,7 @@ pub(crate) async fn add_contact_to_chat_ex( ); ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed"); - if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF as u32).await { + if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await { /* we should respect this - whatever we send to the group, it gets discarded anyway! */ emit_event!( context, @@ -2310,7 +2318,7 @@ pub(crate) async fn add_contact_to_chat_ex( } let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .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. @@ -2343,8 +2351,10 @@ pub(crate) async fn add_contact_to_chat_ex( } if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 { msg.viewtype = Viewtype::Text; - msg.text = - Some(stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF).await); + + msg.text = Some( + stock_str::msg_add_member(context, 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()); @@ -2363,16 +2373,14 @@ pub(crate) async fn reset_gossiped_timestamp( /// Get timestamp of the last gossip sent in the chat. /// Zero return value means that gossip was never sent. -pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> i64 { - context +pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result { + let timestamp = context .sql - .query_get_value::( - context, - "SELECT gossiped_timestamp FROM chats WHERE id=?;", - paramsv![chat_id], + .query_get_value( + sqlx::query("SELECT gossiped_timestamp FROM chats WHERE id=?;").bind(chat_id), ) - .await - .unwrap_or_default() + .await?; + Ok(timestamp.unwrap_or_default()) } pub(crate) async fn set_gossiped_timestamp( @@ -2389,8 +2397,9 @@ pub(crate) async fn set_gossiped_timestamp( context .sql .execute( - "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", - paramsv![timestamp, chat_id], + sqlx::query("UPDATE chats SET gossiped_timestamp=? WHERE id=?;") + .bind(timestamp) + .bind(chat_id), ) .await?; @@ -2404,38 +2413,33 @@ pub(crate) async fn shall_attach_selfavatar( // 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 - { + if !context.sql.get_raw_config_bool("attach_selfavatar").await? { return Ok(false); } let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60; - let needs_attach = context + let mut rows = context .sql - .query_map( - "SELECT c.selfavatar_sent + .fetch( + sqlx::query( + "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!=?;", - 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) - }, + ) + .bind(chat_id) + .bind(DC_CONTACT_ID_SELF), ) .await?; + + let mut needs_attach = false; + while let Some(row) = rows.next().await { + let row = row?; + let selfavatar_sent: i64 = row.try_get(0)?; + if selfavatar_sent < timestamp_some_days_ago { + needs_attach = true; + } + } Ok(needs_attach) } @@ -2446,35 +2450,50 @@ pub enum MuteDuration { Until(SystemTime), } -impl rusqlite::types::ToSql for MuteDuration { - fn to_sql(&self) -> rusqlite::Result { - let duration: i64 = match &self { - MuteDuration::NotMuted => 0, - MuteDuration::Forever => -1, - MuteDuration::Until(when) => { - let duration = when - .duration_since(SystemTime::UNIX_EPOCH) - .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; - i64::try_from(duration.as_secs()) - .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))? - } - }; - let val = rusqlite::types::Value::Integer(duration); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) +impl sqlx::Type for MuteDuration { + fn type_info() -> sqlx::sqlite::SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { + >::compatible(ty) } } -impl rusqlite::types::FromSql for MuteDuration { - fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { +impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for MuteDuration { + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> sqlx::encode::IsNull { + let duration: i64 = match &self { + MuteDuration::NotMuted => 0, + MuteDuration::Forever => -1, + MuteDuration::Until(when) => when + .duration_since(SystemTime::UNIX_EPOCH) + .ok() + .and_then(|d| d.as_secs().try_into().ok()) + .unwrap_or(0), + }; + + args.push(sqlx::sqlite::SqliteArgumentValue::Int64(duration)); + + sqlx::encode::IsNull::No + } +} + +impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for MuteDuration { + fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let value: i64 = sqlx::Decode::decode(value)?; // Negative values other than -1 should not be in the // database. If found they'll be NotMuted. - match i64::column_result(value)? { + match value { 0 => Ok(MuteDuration::NotMuted), -1 => Ok(MuteDuration::Forever), n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) { Some(t) => Ok(MuteDuration::Until(t)), - None => Err(rusqlite::types::FromSqlError::OutOfRange(n)), + None => Err(Box::new(sqlx::error::Error::Decode(Box::new( + crate::error::OutOfRangeError, + )))), }, _ => Ok(MuteDuration::NotMuted), } @@ -2490,8 +2509,9 @@ pub async fn set_muted( if context .sql .execute( - "UPDATE chats SET muted_until=? WHERE id=?;", - paramsv![duration, chat_id], + sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;") + .bind(duration) + .bind(chat_id), ) .await .is_ok() @@ -2538,14 +2558,15 @@ pub async fn remove_contact_from_chat( msg.viewtype = Viewtype::Text; if contact.id == DC_CONTACT_ID_SELF { set_group_explicitly_left(context, chat.grpid).await?; - msg.text = - Some(stock_str::msg_group_left(context, DC_CONTACT_ID_SELF).await); + msg.text = Some( + stock_str::msg_group_left(context, DC_CONTACT_ID_SELF as u32).await, + ); } else { msg.text = Some( stock_str::msg_del_member( context, contact.get_addr(), - DC_CONTACT_ID_SELF, + DC_CONTACT_ID_SELF as u32, ) .await, ); @@ -2582,10 +2603,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: impl AsRef) -> if !is_group_explicitly_left(context, grpid.as_ref()).await? { context .sql - .execute( - "INSERT INTO leftgrps (grpid) VALUES(?);", - paramsv![grpid.as_ref().to_string()], - ) + .execute(sqlx::query("INSERT INTO leftgrps (grpid) VALUES(?);").bind(grpid.as_ref())) .await?; } @@ -2596,14 +2614,11 @@ pub(crate) async fn is_group_explicitly_left( context: &Context, grpid: impl AsRef, ) -> Result { - context + let exists = context .sql - .exists( - "SELECT id FROM leftgrps WHERE grpid=?;", - paramsv![grpid.as_ref()], - ) - .await - .map_err(Into::into) + .exists(sqlx::query("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;").bind(grpid.as_ref())) + .await?; + Ok(exists) } pub async fn set_chat_name( @@ -2634,8 +2649,9 @@ pub async fn set_chat_name( if context .sql .execute( - "UPDATE chats SET name=? WHERE id=?;", - paramsv![new_name.to_string(), chat_id], + sqlx::query("UPDATE chats SET name=? WHERE id=?;") + .bind(new_name.to_string()) + .bind(chat_id), ) .await .is_ok() @@ -2643,8 +2659,13 @@ pub async fn set_chat_name( if chat.is_promoted() && !chat.is_mailing_list() { msg.viewtype = Viewtype::Text; msg.text = Some( - stock_str::msg_grp_name(context, &chat.name, &new_name, DC_CONTACT_ID_SELF) - .await, + stock_str::msg_grp_name( + context, + &chat.name, + &new_name, + DC_CONTACT_ID_SELF as u32, + ) + .await, ); msg.param.set_cmd(SystemMessage::GroupNameChanged); if !chat.name.is_empty() { @@ -2701,7 +2722,7 @@ pub async 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(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF).await); + msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF as u32).await); } else { let image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) { Ok(blob) => Ok(blob), @@ -2715,7 +2736,7 @@ pub async fn set_chat_profile_image( image_blob.recode_to_avatar_size(context).await?; chat.param.set(Param::ProfileImage, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name()); - msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF).await); + msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF as u32).await); } chat.update_param(context).await?; if chat.is_promoted() && !chat.is_mailing_list() { @@ -2748,21 +2769,20 @@ pub async fn forward_msgs( 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()).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?; + let q = format!( + "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", + msg_ids.iter().map(|_| "?").join(",") + ); + let mut query = sqlx::query(&q); + for v in msg_ids { + query = query.bind(v); + } - for id in ids { - let src_msg_id: MsgId = id; + let mut rows = context.sql.fetch(query).await?; + + while let Some(row) = rows.next().await { + let row = row?; + let src_msg_id: MsgId = row.try_get(0)?; let msg = Message::load_from_db(context, src_msg_id).await; if msg.is_err() { break; @@ -2824,32 +2844,27 @@ pub async fn forward_msgs( Ok(()) } -pub(crate) async fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize { - context +pub(crate) async fn get_chat_contact_cnt( + context: &Context, + chat_id: ChatId, +) -> Result { + let count = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;", - paramsv![chat_id], - ) - .await - .unwrap_or_default() as usize + .count(sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;").bind(chat_id)) + .await?; + Ok(count as usize) } -pub(crate) async fn get_chat_cnt(context: &Context) -> usize { +pub(crate) async fn get_chat_cnt(context: &Context) -> Result { if context.sql.is_open().await { - /* no database, no chats - this is no error (needed eg. for information) */ - context + // no database, no chats - this is no error (needed eg. for information) + let count = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;", - paramsv![], - ) - .await - .unwrap_or_default() as usize + .count("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;") + .await?; + Ok(count as usize) } else { - 0 + Ok(0) } } @@ -2858,22 +2873,23 @@ pub(crate) async fn get_chat_id_by_grpid( context: &Context, grpid: impl AsRef, ) -> Result<(ChatId, bool, Blocked), sql::Error> { - context + let (chat_id, b, p) = context .sql - .query_row( - "SELECT id, blocked, protected 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 p = row - .get::<_, Option>(2)? - .unwrap_or_default(); - Ok((chat_id, p == ProtectionStatus::Protected, b)) - }, + .fetch_one( + sqlx::query("SELECT id, blocked, protected FROM chats WHERE grpid=?;") + .bind(grpid.as_ref()), ) .await + .and_then(|row| { + Ok(( + row.try_get(0)?, + row.try_get::, _>(1)?.unwrap_or_default(), + row.try_get::, _>(2)? + .unwrap_or_default(), + )) + })?; + + Ok((chat_id, p == ProtectionStatus::Protected, b)) } /// Adds a message to device chat. @@ -2918,49 +2934,60 @@ pub async fn add_device_msg_with_importance( if let Some(last_msg_time) = context .sql .query_get_value( - context, - "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", - paramsv![chat_id], + sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?").bind(chat_id), ) - .await + .await? { if timestamp_sort <= last_msg_time { timestamp_sort = last_msg_time + 1; } } - context.sql.execute( - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,timestamp_sent,timestamp_rcvd,type,state, txt,param,rfc724_mid) \ - VALUES (?,?,?, ?,?,?,?,?, ?,?,?);", - paramsv![ - chat_id, - DC_CONTACT_ID_DEVICE, - DC_CONTACT_ID_SELF, - timestamp_sort, - timestamp_sent, - timestamp_sent, // timestamp_sent equals timestamp_rcvd - msg.viewtype, - MessageState::InFresh, - msg.text.as_ref().cloned().unwrap_or_default(), - msg.param.to_string(), - rfc724_mid, - ], - ).await?; + context + .sql + .execute( + sqlx::query( + "INSERT INTO msgs ( + chat_id, + from_id, + to_id, + timestamp, + timestamp_sent, + timestamp_rcvd, + type,state, + txt, + param, + rfc724_mid) + VALUES (?,?,?,?,?,?,?,?,?,?,?);", + ) + .bind(chat_id) + .bind(DC_CONTACT_ID_DEVICE as i32) + .bind(DC_CONTACT_ID_SELF as i32) + .bind(timestamp_sort) + .bind(timestamp_sent) + .bind(timestamp_sent) + .bind( + // timestamp_sent equals timestamp_rcvd + msg.viewtype, + ) + .bind(MessageState::InFresh) + .bind(msg.text.as_ref().cloned().unwrap_or_default()) + .bind(msg.param.to_string()) + .bind(&rfc724_mid), + ) + .await?; let row_id = context .sql - .get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid) + .get_rowid("msgs", "rfc724_mid", &rfc724_mid) .await?; - msg_id = MsgId::new(row_id); + msg_id = MsgId::new(u32::try_from(row_id)?); } if let Some(label) = label { context .sql - .execute( - "INSERT INTO devmsglabels (label) VALUES (?);", - paramsv![label.to_string()], - ) + .execute(sqlx::query("INSERT INTO devmsglabels (label) VALUES (?);").bind(label)) .await?; } @@ -2985,19 +3012,12 @@ pub async fn add_device_msg( pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result { ensure!(!label.is_empty(), "empty label"); - if let Ok(()) = context + let exists = context .sql - .query_row( - "SELECT label FROM devmsglabels WHERE label=?", - paramsv![label], - |_| Ok(()), - ) - .await - { - return Ok(true); - } + .exists(sqlx::query("SELECT COUNT(label) FROM devmsglabels WHERE label=?").bind(label)) + .await?; - Ok(false) + Ok(exists) } // needed on device-switches during export/import; @@ -3008,15 +3028,9 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> { context .sql - .execute( - "DELETE FROM msgs WHERE from_id=?;", - paramsv![DC_CONTACT_ID_DEVICE], - ) - .await?; - context - .sql - .execute("DELETE FROM devmsglabels;", paramsv![]) + .execute(sqlx::query("DELETE FROM msgs WHERE from_id=?;").bind(DC_CONTACT_ID_DEVICE as i32)) .await?; + context.sql.execute("DELETE FROM devmsglabels;").await?; Ok(()) } @@ -3038,27 +3052,25 @@ pub(crate) async fn add_info_msg_with_cmd( } context.sql.execute( - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);", - paramsv![ - chat_id, - DC_CONTACT_ID_INFO, - DC_CONTACT_ID_INFO, - dc_create_smeared_timestamp(context).await, - Viewtype::Text, - MessageState::InNoticed, - text.as_ref().to_string(), - rfc724_mid, - ephemeral_timer, - param.to_string(), - ] + sqlx::query("INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);") + .bind(chat_id) + .bind(DC_CONTACT_ID_INFO as i32) + .bind(DC_CONTACT_ID_INFO as i32) + .bind(dc_create_smeared_timestamp(context).await) + .bind(Viewtype::Text) + .bind(MessageState::InNoticed) + .bind(text.as_ref().to_string()) + .bind(&rfc724_mid) + .bind(ephemeral_timer) + .bind(param.to_string()) ).await?; let row_id = context .sql - .get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid) + .get_rowid("msgs", "rfc724_mid", &rfc724_mid) .await .unwrap_or_default(); - let msg_id = MsgId::new(row_id); + let msg_id = MsgId::new(u32::try_from(row_id)?); context.emit_event(EventType::MsgsChanged { chat_id, msg_id }); Ok(msg_id) } @@ -3121,10 +3133,7 @@ mod tests { #[async_std::test] async fn test_get_draft_special_chat_id() { let t = TestContext::new().await; - let draft = ChatId::new(DC_CHAT_ID_LAST_SPECIAL) - .get_draft(&t) - .await - .unwrap(); + let draft = DC_CHAT_ID_LAST_SPECIAL.get_draft(&t).await.unwrap(); assert!(draft.is_none()); } @@ -3143,7 +3152,8 @@ mod tests { let chat_id = &t.get_self_chat().await.id; let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("hello".to_string())); - chat_id.set_draft(&t, Some(&mut msg)).await; + + chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); let draft = chat_id.get_draft(&t).await.unwrap().unwrap(); let msg_text = msg.get_text(); let draft_text = draft.get_text(); @@ -3170,21 +3180,21 @@ mod tests { let chat_id = create_by_contact_id(&ctx, bob).await.unwrap(); let chat = Chat::load_from_db(&ctx, chat_id).await.unwrap(); assert_eq!(chat.typ, Chattype::Single); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.len(), 1); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); // adding or removing contacts from one-to-one-chats result in an error let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); let added = add_contact_to_chat_ex(&ctx, chat.id, claire, false).await; assert!(added.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.len(), 1); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); let removed = remove_contact_from_chat(&ctx, chat.id, claire).await; assert!(removed.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.len(), 1); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); let removed = remove_contact_from_chat(&ctx, chat.id, DC_CONTACT_ID_SELF).await; assert!(removed.is_err()); - assert_eq!(get_chat_contacts(&ctx, chat.id).await.len(), 1); + assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); } #[async_std::test] @@ -3198,16 +3208,14 @@ mod tests { assert!(!chat.is_device_talk()); assert!(chat.can_send()); assert_eq!(chat.name, stock_str::saved_messages(&t).await); - assert!(chat.get_profile_image(&t).await.is_some()); + assert!(chat.get_profile_image(&t).await.unwrap().is_some()); } #[async_std::test] async fn test_deaddrop_chat() { let t = TestContext::new().await; - let chat = Chat::load_from_db(&t, ChatId::new(DC_CHAT_ID_DEADDROP)) - .await - .unwrap(); - assert_eq!(DC_CHAT_ID_DEADDROP, 1); + let chat = Chat::load_from_db(&t, DC_CHAT_ID_DEADDROP).await.unwrap(); + assert_eq!(DC_CHAT_ID_DEADDROP.0, 1); assert!(chat.id.is_deaddrop()); assert!(!chat.is_self_talk()); assert!(chat.visibility == ChatVisibility::Normal); @@ -3248,7 +3256,7 @@ mod tests { assert_eq!(msg2.text.as_ref().unwrap(), "second message"); // check device chat - assert_eq!(msg2.chat_id.get_msg_cnt(&t).await, 2); + assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2); } #[async_std::test] @@ -3281,7 +3289,8 @@ mod tests { // check device chat let chat_id = msg1.chat_id; - assert_eq!(chat_id.get_msg_cnt(&t).await, 1); + + assert_eq!(chat_id.get_msg_cnt(&t).await.unwrap(), 1); assert!(!chat_id.is_special()); let chat = Chat::load_from_db(&t, chat_id).await; assert!(chat.is_ok()); @@ -3290,8 +3299,9 @@ mod tests { assert!(chat.is_device_talk()); assert!(!chat.is_self_talk()); assert!(!chat.can_send()); + assert_eq!(chat.name, stock_str::device_messages(&t).await); - assert!(chat.get_profile_image(&t).await.is_some()); + assert!(chat.get_profile_image(&t).await.unwrap().is_some()); // delete device message, make sure it is not added again message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await; @@ -3426,7 +3436,8 @@ mod tests { let chat_id2 = t.get_self_chat().await.id; assert!(!chat_id1.is_special()); assert!(!chat_id2.is_special()); - assert_eq!(get_chat_cnt(&t).await, 2); + + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); assert_eq!(chatlist_len(&t, 0).await, 2); assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 2); assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 0); @@ -3452,7 +3463,7 @@ mod tests { .get_visibility() == ChatVisibility::Normal ); - assert_eq!(get_chat_cnt(&t).await, 2); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); assert_eq!(chatlist_len(&t, 0).await, 2); // including DC_CHAT_ID_ARCHIVED_LINK now assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); @@ -3476,7 +3487,7 @@ mod tests { .get_visibility() == ChatVisibility::Archived ); - assert_eq!(get_chat_cnt(&t).await, 2); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); assert_eq!(chatlist_len(&t, 0).await, 1); // only DC_CHAT_ID_ARCHIVED_LINK now assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 0); assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 2); @@ -3508,7 +3519,7 @@ mod tests { .get_visibility() == ChatVisibility::Normal ); - assert_eq!(get_chat_cnt(&t).await, 2); + assert_eq!(get_chat_cnt(&t).await.unwrap(), 2); assert_eq!(chatlist_len(&t, 0).await, 2); assert_eq!(chatlist_len(&t, DC_GCL_NO_SPECIALS).await, 1); assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1); @@ -3753,7 +3764,7 @@ mod tests { assert!(chat.is_protected()); assert!(chat.is_unpromoted()); - let msgs = get_chat_msgs(&t, chat_id, 0, None).await; + let msgs = get_chat_msgs(&t, chat_id, 0, None).await.unwrap(); assert_eq!(msgs.len(), 1); let msg = t.get_last_msg_in(chat_id).await; @@ -3783,7 +3794,7 @@ mod tests { assert!(!chat.is_protected()); assert!(!chat.is_unpromoted()); - let msgs = get_chat_msgs(&t, chat_id, 0, None).await; + let msgs = get_chat_msgs(&t, chat_id, 0, None).await.unwrap(); assert_eq!(msgs.len(), 3); // enable protection on promoted chat, the info-message is sent via send_msg() this time @@ -3855,6 +3866,7 @@ mod tests { // Alice creates a group with Bob, sends a message to bob let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; + alice .set_config(Config::ShowEmails, Some("2")) .await @@ -3869,13 +3881,30 @@ mod tests { .await .unwrap(); let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await.unwrap(); + + println!("----- add_contact_to_chat"); add_contact_to_chat(&alice, alice_chat_id, contact_id).await; - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await.len(), 2); + assert_eq!( + get_chat_contacts(&alice, alice_chat_id) + .await + .unwrap() + .len(), + 2 + ); + println!("----- send_text_msg"); send_text_msg(&alice, alice_chat_id, "hi!".to_string()) .await .ok(); - assert_eq!(get_chat_msgs(&alice, alice_chat_id, 0, None).await.len(), 1); + println!("----- get_chat_msgs"); + assert_eq!( + get_chat_msgs(&alice, alice_chat_id, 0, None) + .await + .unwrap() + .len(), + 1 + ); + println!("----- pop_sent_msg"); // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com). let msg = alice.pop_sent_msg().await.payload(); assert_eq!(msg.match_indices("Gr.").count(), 2); @@ -3888,9 +3917,11 @@ mod tests { .unwrap(); let msg = bob.get_last_msg().await; + println!("load from dbb"); let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap(); assert_eq!(bob_chat.grpid, alice_chat.grpid); + println!("chat loaded"); // Bob answers - simulate a normal MUA by not setting `Chat-*`-headers; // moreover, Bob's SMTP-server also replaces the `Message-ID:`-header send_text_msg(&bob, bob_chat.id, "ho!".to_string()) @@ -3901,13 +3932,21 @@ mod tests { let msg = msg.replace("Chat-", "XXXX-"); assert_eq!(msg.match_indices("Chat-").count(), 0); + println!("last receive start"); // Alice receives this message - she can still detect the group by the `References:`-header dc_receive_imf(&alice, msg.as_bytes(), "INBOX", 2, false) .await .unwrap(); + println!("----- last receie if"); let msg = alice.get_last_msg().await; assert_eq!(msg.chat_id, alice_chat_id); assert_eq!(msg.text, Some("ho!".to_string())); - assert_eq!(get_chat_msgs(&alice, alice_chat_id, 0, None).await.len(), 2); + assert_eq!( + get_chat_msgs(&alice, alice_chat_id, 0, None) + .await + .unwrap() + .len(), + 2 + ); } } diff --git a/src/chatlist.rs b/src/chatlist.rs index 2a9367688..fc0133b3f 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,6 +1,8 @@ //! # Chat list module use anyhow::{bail, ensure, Result}; +use async_std::prelude::*; +use sqlx::Row; use crate::chat; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; @@ -110,17 +112,6 @@ impl Chatlist { let mut add_archived_link_item = false; - let process_row = |row: &rusqlite::Row| { - let chat_id: ChatId = row.get(0)?; - let msg_id: MsgId = row.get(1).unwrap_or_default(); - Ok((chat_id, msg_id)) - }; - - let process_rows = |rows: rusqlite::MappedRows<_>| { - rows.collect::, _>>() - .map_err(Into::into) - }; - let skip_id = if flag_for_forwarding { chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) .await @@ -130,6 +121,13 @@ impl Chatlist { ChatId::new(0) }; + let process_row = |row: sqlx::Result| { + let row = row?; + let chat_id: ChatId = row.try_get(0)?; + let msg_id: MsgId = row.try_get(1).unwrap_or_default(); + Ok((chat_id, msg_id)) + }; + // select with left join and minimum: // // - the inner select must use `hidden` and _not_ `m.hidden` @@ -145,10 +143,10 @@ impl Chatlist { // tg do the same) for the deaddrop, however, they should // really be hidden, however, _currently_ the deaddrop is not // shown at all permanent in the chatlist. - let mut ids = if let Some(query_contact_id) = query_contact_id { + let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id { // show chats shared with a given contact - context.sql.query_map( - "SELECT c.id, m.id + context.sql.fetch( + sqlx::query("SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -162,11 +160,9 @@ impl Chatlist { AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2) GROUP BY c.id - ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned], - process_row, - process_rows, - ).await? + ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;" + ).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned) + ).await?.map(process_row).collect::>().await? } else if flag_archived_only { // show archived chats // (this includes the archived device-chat; we could skip it, @@ -174,8 +170,9 @@ impl Chatlist { // and adapting the number requires larger refactorings and seems not to be worth the effort) context .sql - .query_map( - "SELECT c.id, m.id + .fetch( + sqlx::query( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -190,11 +187,13 @@ impl Chatlist { AND c.archived=1 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft], - process_row, - process_rows, + ) + .bind(MessageState::OutDraft), ) .await? + .map(process_row) + .collect::>() + .await? } else if let Some(query) = query { let query = query.trim().to_string(); ensure!(!query.is_empty(), "missing query"); @@ -208,8 +207,9 @@ impl Chatlist { let str_like_cmd = format!("%{}%", query); context .sql - .query_map( - "SELECT c.id, m.id + .fetch( + sqlx::query( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -224,11 +224,15 @@ impl Chatlist { AND c.name LIKE ?3 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, skip_id, str_like_cmd], - process_row, - process_rows, + ) + .bind(MessageState::OutDraft) + .bind(skip_id) + .bind(str_like_cmd), ) .await? + .map(process_row) + .collect::>() + .await? } else { // show normal chatlist let sort_id_up = if flag_for_forwarding { @@ -239,7 +243,8 @@ impl Chatlist { } else { ChatId::new(0) }; - let mut ids = context.sql.query_map( + + let mut ids: Vec<_> = context.sql.fetch(sqlx::query( "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m @@ -254,19 +259,21 @@ impl Chatlist { AND c.blocked=0 AND NOT c.archived=?3 GROUP BY c.id - ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], - process_row, - process_rows, - ).await?; + ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;" + ) + .bind(MessageState::OutDraft) + .bind(skip_id) + .bind(ChatVisibility::Archived) + .bind(sort_id_up) + .bind(ChatVisibility::Pinned) + ).await?.map(process_row).collect::>().await?; + if !flag_no_specials { - if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await + if let Some(last_deaddrop_fresh_msg_id) = + get_last_deaddrop_fresh_msg(context).await? { if !flag_for_forwarding { - ids.insert( - 0, - (ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id), - ); + ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id)); } } add_archived_link_item = true; @@ -274,11 +281,11 @@ impl Chatlist { ids }; - if add_archived_link_item && dc_get_archived_cnt(context).await > 0 { + if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 { if ids.is_empty() && flag_add_alldone_hint { - ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0))); + ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0))); } - ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0))); + ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0))); } Ok(Chatlist { ids }) @@ -400,38 +407,31 @@ impl Chatlist { } /// Returns the number of archived chats -pub async fn dc_get_archived_cnt(context: &Context) -> u32 { - context +pub async fn dc_get_archived_cnt(context: &Context) -> Result { + let count = context .sql - .query_get_value( - context, - "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", - paramsv![], - ) - .await - .unwrap_or_default() + .count("SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;") + .await?; + Ok(count) } -async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option { +async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result> { // We have an index over the state-column, this should be // sufficient as there are typically only few fresh messages. - context + let id = context .sql - .query_get_value( - context, - concat!( - "SELECT m.id", - " FROM msgs m", - " LEFT JOIN chats c", - " ON c.id=m.chat_id", - " WHERE m.state=10", - " AND m.hidden=0", - " AND c.blocked=2", - " ORDER BY m.timestamp DESC, m.id DESC;" - ), - paramsv![], - ) - .await + .query_get_value(concat!( + "SELECT m.id", + " FROM msgs m", + " LEFT JOIN chats c", + " ON c.id=m.chat_id", + " WHERE m.state=10", + " AND m.hidden=0", + " AND c.blocked=2", + " ORDER BY m.timestamp DESC, m.id DESC;" + )) + .await?; + Ok(id) } #[cfg(test)] @@ -466,7 +466,7 @@ mod tests { // drafts are sorted to the top let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("hello".to_string())); - chat_id2.set_draft(&t, Some(&mut msg)).await; + chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.get_chat_id(0), chat_id2); @@ -554,7 +554,7 @@ mod tests { let mut msg = Message::new(Viewtype::Text); msg.set_text(Some("foo:\nbar \r\n test".to_string())); - chat_id1.set_draft(&t, Some(&mut msg)).await; + chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let summary = chats.get_summary(&t, 0, None).await; diff --git a/src/config.rs b/src/config.rs index ddc1f59f7..4d2e90ad4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ //! # Key-value configuration management +use anyhow::Result; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; @@ -150,69 +151,66 @@ pub enum Config { } impl Context { - pub async fn config_exists(&self, key: Config) -> bool { - self.sql.get_raw_config(self, key).await.is_some() + pub async fn config_exists(&self, key: Config) -> Result { + Ok(self.sql.get_raw_config(key).await?.is_some()) } /// Get a configuration key. Returns `None` if no value is set, and no default value found. - pub async fn get_config(&self, key: Config) -> Option { + pub async fn get_config(&self, key: Config) -> Result> { let value = match key { Config::Selfavatar => { - let rel_path = self.sql.get_raw_config(self, key).await; + let rel_path = self.sql.get_raw_config(key).await?; rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned()) } Config::SysVersion => Some((&*DC_VERSION_STR).clone()), Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)), Config::SysConfigKeys => Some(get_config_keys_string()), - _ => self.sql.get_raw_config(self, key).await, + _ => self.sql.get_raw_config(key).await?, }; if value.is_some() { - return value; + return Ok(value); } // Default values match key { - Config::Selfstatus => Some(stock_str::status_line(self).await), - Config::ConfiguredInboxFolder => Some("INBOX".to_owned()), - _ => key.get_str("default").map(|s| s.to_string()), + Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)), + Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())), + _ => Ok(key.get_str("default").map(|s| s.to_string())), } } - pub async fn get_config_int(&self, key: Config) -> i32 { + pub async fn get_config_int(&self, key: Config) -> Result { self.get_config(key) .await - .and_then(|s| s.parse().ok()) - .unwrap_or_default() + .map(|s: Option| s.and_then(|s| s.parse().ok()).unwrap_or_default()) } - pub async fn get_config_i64(&self, key: Config) -> i64 { + pub async fn get_config_i64(&self, key: Config) -> Result { self.get_config(key) .await - .and_then(|s| s.parse().ok()) - .unwrap_or_default() + .map(|s: Option| s.and_then(|s| s.parse().ok()).unwrap_or_default()) } - pub async fn get_config_u64(&self, key: Config) -> u64 { + pub async fn get_config_u64(&self, key: Config) -> Result { self.get_config(key) .await - .and_then(|s| s.parse().ok()) - .unwrap_or_default() + .map(|s: Option| s.and_then(|s| s.parse().ok()).unwrap_or_default()) } - pub async fn get_config_bool(&self, key: Config) -> bool { - self.get_config_int(key).await != 0 + pub async fn get_config_bool(&self, key: Config) -> Result { + Ok(self.get_config_int(key).await? != 0) } /// Gets configured "delete_server_after" value. /// /// `None` means never delete the message, `Some(0)` means delete /// at once, `Some(x)` means delete after `x` seconds. - pub async fn get_config_delete_server_after(&self) -> Option { - match self.get_config_int(Config::DeleteServerAfter).await { - 0 => None, - 1 => Some(0), - x => Some(x as i64), + pub async fn get_config_delete_server_after(&self) -> Result> { + match self.get_config_int(Config::DeleteServerAfter).await? { + 0 => Ok(None), + 1 => Ok(Some(0)), + x => Ok(Some(x as i64)), } } @@ -220,41 +218,46 @@ impl Context { /// /// The provider is determined by `get_provider_info()` during configuration and then saved /// to the db in `param.save_to_database()`, together with all the other `configured_*` values. - pub async fn get_configured_provider(&self) -> Option<&'static Provider> { - get_provider_by_id(&self.get_config(Config::ConfiguredProvider).await?) + pub async fn get_configured_provider(&self) -> Result> { + if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? { + return Ok(get_provider_by_id(&cfg)); + } + Ok(None) } /// Gets configured "delete_device_after" value. /// /// `None` means never delete the message, `Some(x)` means delete /// after `x` seconds. - pub async fn get_config_delete_device_after(&self) -> Option { - match self.get_config_int(Config::DeleteDeviceAfter).await { - 0 => None, - x => Some(x as i64), + pub async fn get_config_delete_device_after(&self) -> Result> { + match self.get_config_int(Config::DeleteDeviceAfter).await? { + 0 => Ok(None), + x => Ok(Some(x as i64)), } } /// Set the given config key. /// If `None` is passed as a value the value is cleared and set to the default if there is one. - pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> { + pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> { match key { Config::Selfavatar => { self.sql - .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![]) + .execute("UPDATE contacts SET selfavatar_sent=0;") .await?; self.sql - .set_raw_config_bool(self, "attach_selfavatar", true) + .set_raw_config_bool("attach_selfavatar", true) .await?; match value { Some(value) => { let blob = BlobObject::new_from_path(self, value).await?; blob.recode_to_avatar_size(self).await?; - self.sql - .set_raw_config(self, key, Some(blob.as_name())) - .await + self.sql.set_raw_config(key, Some(blob.as_name())).await?; + Ok(()) + } + None => { + self.sql.set_raw_config(key, None).await?; + Ok(()) } - None => self.sql.set_raw_config(self, key, None).await, } } Config::Selfstatus => { @@ -265,10 +268,15 @@ impl Context { value }; - self.sql.set_raw_config(self, key, val).await + self.sql.set_raw_config(key, val).await?; + Ok(()) } Config::DeleteDeviceAfter => { - let ret = self.sql.set_raw_config(self, key, value).await; + let ret = self + .sql + .set_raw_config(key, value) + .await + .map_err(Into::into); // Force chatlist reload to delete old messages immediately. self.emit_event(EventType::MsgsChanged { msg_id: MsgId::new(0), @@ -278,20 +286,29 @@ impl Context { } Config::Displayname => { let value = value.map(improve_single_line_input); - self.sql.set_raw_config(self, key, value.as_deref()).await + self.sql.set_raw_config(key, value.as_deref()).await?; + Ok(()) } Config::DeleteServerAfter => { - let ret = self.sql.set_raw_config(self, key, value).await; + let ret = self + .sql + .set_raw_config(key, value) + .await + .map_err(Into::into); job::schedule_resync(self).await; ret } - _ => self.sql.set_raw_config(self, key, value).await, + _ => { + self.sql.set_raw_config(key, value).await?; + Ok(()) + } } } pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> { self.set_config(key, if value { Some("1") } else { None }) - .await + .await?; + Ok(()) } } @@ -349,7 +366,7 @@ mod tests { .unwrap(); assert!(avatar_blob.exists().await); assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64); - let avatar_cfg = t.get_config(Config::Selfavatar).await; + let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap(); assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); let img = image::open(avatar_src).unwrap(); @@ -378,7 +395,7 @@ mod tests { t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) .await .unwrap(); - let avatar_cfg = t.get_config(Config::Selfavatar).await; + let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap(); assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string())); let img = image::open(avatar_src).unwrap(); @@ -405,21 +422,21 @@ mod tests { std::fs::metadata(&avatar_blob).unwrap().len(), avatar_bytes.len() as u64 ); - let avatar_cfg = t.get_config(Config::Selfavatar).await; + let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap(); assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); } #[async_std::test] async fn test_media_quality_config_option() { let t = TestContext::new().await; - let media_quality = t.get_config_int(Config::MediaQuality).await; + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); assert_eq!(media_quality, 0); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); assert_eq!(media_quality, constants::MediaQuality::Balanced); t.set_config(Config::MediaQuality, Some("1")).await.unwrap(); - let media_quality = t.get_config_int(Config::MediaQuality).await; + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); assert_eq!(media_quality, 1); assert_eq!(constants::MediaQuality::Worse as i32, 1); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); diff --git a/src/configure/mod.rs b/src/configure/mod.rs index 14930d334..06d8cbe00 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -50,8 +50,11 @@ macro_rules! progress { impl Context { /// Checks if the context is already configured. - pub async fn is_configured(&self) -> bool { - self.sql.get_raw_config_bool(self, "configured").await + pub async fn is_configured(&self) -> Result { + self.sql + .get_raw_config_bool("configured") + .await + .map_err(Into::into) } /// Configures this account with the currently set parameters. @@ -84,14 +87,14 @@ impl Context { async fn inner_configure(&self) -> Result<()> { info!(self, "Configure ..."); - let mut param = LoginParam::from_database(self, "").await; + let mut param = LoginParam::from_database(self, "").await?; let success = configure(self, &mut param).await; self.set_config(Config::NotifyAboutWrongPw, None).await?; if let Some(provider) = param.provider { if let Some(config_defaults) = &provider.config_defaults { for def in config_defaults.iter() { - if !self.config_exists(def.key).await { + if !self.config_exists(def.key).await? { info!(self, "apply config_defaults {}={}", def.key, def.value); self.set_config(def.key, Some(def.value)).await?; } else { @@ -177,13 +180,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { // if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one. progress!(ctx, 10); if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password) - .await + .await? .and_then(|e| e.parse().ok()) { info!(ctx, "Authorized address is {}", oauth2_addr); param.addr = oauth2_addr; ctx.sql - .set_raw_config(ctx, "addr", Some(param.addr.as_str())) + .set_raw_config("addr", Some(param.addr.as_str())) .await?; } progress!(ctx, 20); @@ -397,8 +400,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { progress!(ctx, 900); - let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await - || ctx.get_config_bool(Config::MvboxMove).await; + let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await? + || ctx.get_config_bool(Config::MvboxMove).await?; imap.configure_folders(ctx, create_mvbox).await?; @@ -413,7 +416,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { // "configured_" prefix; also write the "configured"-flag */ // the trailing underscore is correct param.save_to_database(ctx, "configured_").await?; - ctx.sql.set_raw_config_bool(ctx, "configured", true).await?; + ctx.sql.set_raw_config_bool("configured", true).await?; ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string())) .await?; diff --git a/src/constants.rs b/src/constants.rs index c14a08960..97f65f99e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,8 +1,9 @@ //! # Constants -use deltachat_derive::{FromSql, ToSql}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use crate::chat::ChatId; + pub static DC_VERSION_STR: Lazy = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string()); #[derive( @@ -14,12 +15,11 @@ pub static DC_VERSION_STR: Lazy = Lazy::new(|| env!("CARGO_PKG_VERSION") Eq, FromPrimitive, ToPrimitive, - FromSql, - ToSql, Serialize, Deserialize, + sqlx::Type, )] -#[repr(u8)] +#[repr(i8)] pub enum Blocked { Not = 0, Manually = 1, @@ -32,9 +32,7 @@ impl Default for Blocked { } } -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[repr(u8)] pub enum ShowEmails { Off = 0, @@ -48,9 +46,7 @@ impl Default for ShowEmails { } } -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[repr(u8)] pub enum MediaQuality { Balanced = 0, @@ -63,9 +59,7 @@ impl Default for MediaQuality { } } -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[repr(u8)] pub enum KeyGenType { Default = 0, @@ -79,9 +73,7 @@ impl Default for KeyGenType { } } -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[repr(i8)] pub enum VideochatType { Unknown = 0, @@ -122,15 +114,15 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; pub const DC_OUTDATED_WARNING_DAYS: i64 = 365; /// virtual chat showing all messages belonging to chats flagged with chats.blocked=2 -pub const DC_CHAT_ID_DEADDROP: u32 = 1; +pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1); /// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again) -pub const DC_CHAT_ID_TRASH: u32 = 3; +pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3); /// only an indicator in a chatlist -pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6; +pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6); /// only an indicator in a chatlist -pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7; +pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7); /// larger chat IDs are "real" chats, their messages are "real" messages. -pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9; +pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); #[derive( Debug, @@ -141,11 +133,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9; Eq, FromPrimitive, ToPrimitive, - FromSql, - ToSql, IntoStaticStr, Serialize, Deserialize, + sqlx::Type, )] #[repr(u32)] pub enum Chattype { @@ -256,12 +247,11 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; Eq, FromPrimitive, ToPrimitive, - FromSql, - ToSql, Serialize, Deserialize, + sqlx::Type, )] -#[repr(i32)] +#[repr(u32)] pub enum Viewtype { Unknown = 0, diff --git a/src/contact.rs b/src/contact.rs index 406969c52..4fb97e311 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,11 +1,13 @@ //! Contacts module +use std::convert::TryFrom; -use anyhow::{bail, ensure, format_err, Context as _, Result}; +use anyhow::{bail, ensure, format_err, Result}; use async_std::path::PathBuf; -use deltachat_derive::{FromSql, ToSql}; +use async_std::prelude::*; use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; +use sqlx::Row; use crate::aheader::EncryptPreference; use crate::chat::ChatId; @@ -77,9 +79,9 @@ pub struct Contact { /// Possible origins of a contact. #[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql, + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type, )] -#[repr(i32)] +#[repr(u32)] pub enum Origin { Unknown = 0, @@ -174,43 +176,45 @@ pub enum VerifiedStatus { impl Contact { pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result { - let mut res = context + let row = context .sql - .query_row( - "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status + .fetch_one( + sqlx::query( + "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status FROM contacts c WHERE c.id=?;", - paramsv![contact_id as i32], - |row| { - let contact = Self { - id: contact_id, - name: row.get::<_, String>(0)?, - authname: row.get::<_, String>(4)?, - addr: row.get::<_, String>(1)?, - blocked: row.get::<_, Option>(3)?.unwrap_or_default() != 0, - origin: row.get(2)?, - param: row.get::<_, String>(5)?.parse().unwrap_or_default(), - status: row.get(6).unwrap_or_default(), - }; - Ok(contact) - }, + ) + .bind(contact_id), ) .await?; + + let mut contact = Contact { + id: contact_id, + name: row.try_get(0)?, + authname: row.try_get(4)?, + addr: row.try_get(1)?, + blocked: row.try_get::, _>(3)?.unwrap_or_default() != 0, + origin: row.try_get(2)?, + param: row.try_get::(5)?.parse().unwrap_or_default(), + status: row.try_get::, _>(6)?.unwrap_or_default(), + }; + if contact_id == DC_CONTACT_ID_SELF { - res.name = stock_str::self_msg(context).await; - res.addr = context + contact.name = stock_str::self_msg(context).await; + contact.addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); - res.status = context + contact.status = context .get_config(Config::Selfstatus) - .await + .await? .unwrap_or_default(); } else if contact_id == DC_CONTACT_ID_DEVICE { - res.name = stock_str::device_messages(context).await; - res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string(); + contact.name = stock_str::device_messages(context).await; + contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string(); } - Ok(res) + + Ok(contact) } /// Returns `true` if this contact is blocked. @@ -281,13 +285,15 @@ impl Contact { if context .sql .execute( - "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", - paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh], + sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;") + .bind(MessageState::InNoticed) + .bind(id as i32) + .bind(MessageState::InFresh), ) .await .is_ok() { - context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP))); + context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP)); } } @@ -308,21 +314,27 @@ impl Contact { let addr_normalized = addr_normalize(addr.as_ref()); - if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await { + if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? { if addr_cmp(addr_normalized, addr_self) { return Ok(Some(DC_CONTACT_ID_SELF)); } } - context.sql.query_get_value_result( - "SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;", - paramsv![ - addr_normalized, - DC_CONTACT_ID_LAST_SPECIAL as i32, - min_origin as u32, - ], - ) - .await - .context("lookup_id_by_addr: SQL query failed") + let id = context + .sql + .query_get_value( + sqlx::query( + "SELECT id FROM contacts \ + WHERE addr=?1 COLLATE NOCASE \ + AND id>?2 AND origin>=?3 AND blocked=0;", + ) + .bind(addr_normalized) + .bind(DC_CONTACT_ID_LAST_SPECIAL) + .bind(min_origin), + ) + .await? + .unwrap_or_default(); + + Ok(id) } /// Lookup a contact and create it if it does not exist yet. @@ -367,7 +379,7 @@ impl Contact { let addr = addr_normalize(addr.as_ref()).to_string(); let addr_self = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); if addr_cmp(&addr, addr_self) { @@ -419,25 +431,33 @@ impl Contact { let mut update_addr = false; let mut row_id = 0; - if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row( - "SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;", - paramsv![addr.to_string()], - |row| { - let row_id = row.get(0)?; - let row_name: String = row.get(1)?; - let row_addr: String = row.get(2)?; - let row_origin: Origin = row.get(3)?; - let row_authname: String = row.get(4)?; + if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context + .sql + .fetch_one( + sqlx::query( + "SELECT id, name, addr, origin, authname \ + FROM contacts WHERE addr=? COLLATE NOCASE;", + ) + .bind(addr.to_string()), + ) + .await + .and_then(|row| { + let row_id = row.try_get(0)?; + let row_name: String = row.try_get(1)?; + let row_addr: String = row.try_get(2)?; + let row_origin: Origin = row.try_get(3)?; + let row_authname: String = row.try_get(4)?; Ok((row_id, row_name, row_addr, row_origin, row_authname)) - }, - ) - .await { + }) + { let update_name = manual && name != row_name; - let update_authname = - !manual && name != row_authname && !name.is_empty() && - (origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty()); - + let update_authname = !manual + && name != row_authname + && !name.is_empty() + && (origin >= row_origin + || origin == Origin::IncomingUnknownFrom + || row_authname.is_empty()); row_id = id; if origin as i32 >= row_origin as i32 && addr != row_addr { update_addr = true; @@ -449,43 +469,55 @@ impl Contact { row_name }; - context - .sql - .execute( - "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", - paramsv![ - new_name, - if update_addr { addr.to_string() } else { row_addr }, - if origin > row_origin { - origin - } else { - row_origin - }, - if update_authname { - name.to_string() - } else { - row_authname - }, - row_id - ], - ) - .await - .ok(); + let query = sqlx::query( + "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", + ) + .bind(&new_name) + .bind(if update_addr { + addr.to_string() + } else { + row_addr + }) + .bind(if origin > row_origin { + origin + } else { + row_origin + }) + .bind(if update_authname { + name.to_string() + } else { + row_authname + }) + .bind(row_id); + + context.sql.execute(query).await.ok(); if update_name { // Update the contact name also if it is used as a group name. // This is one of the few duplicated data, however, getting the chat list is easier this way. - let chat_id = context.sql.query_get_value::( - context, - "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", - paramsv![Chattype::Single, row_id] - ).await; + let chat_id = context.sql.query_get_value::<_, u32>( + sqlx::query( + "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)" + ).bind(Chattype::Single).bind(row_id) + ).await?; if let Some(chat_id) = chat_id { - match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await { + match context + .sql + .execute( + sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3") + .bind(&new_name) + .bind(chat_id) + .bind(&new_name), + ) + .await + { Err(err) => warn!(context, "Can't update chat name: {}", err), - Ok(count) => if count > 0 { - // Chat name updated - context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32))); + Ok(count) => { + if count > 0 { + // Chat name updated + context + .emit_event(EventType::ChatModified(ChatId::new(chat_id))); + } } } } @@ -499,21 +531,26 @@ impl Contact { if context .sql .execute( - "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", - paramsv![ - if update_name { name.to_string() } else { "".to_string() }, - addr, - origin, - if update_authname { name.to_string() } else { "".to_string() } - ], + sqlx::query( + "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", + ) + .bind(if update_name { + name.to_string() + } else { + "".to_string() + }) + .bind(&addr) + .bind(origin) + .bind(if update_authname { + name.to_string() + } else { + "".to_string() + }), ) .await .is_ok() { - row_id = context - .sql - .get_rowid(context, "contacts", "addr", &addr) - .await?; + row_id = context.sql.get_rowid("contacts", "addr", &addr).await?; sth_modified = Modifier::Created; info!(context, "added contact id={} addr={}", row_id, &addr); } else { @@ -521,7 +558,7 @@ impl Contact { } } - Ok((row_id, sth_modified)) + Ok((u32::try_from(row_id)?, sth_modified)) } /// Add a number of contacts. @@ -584,7 +621,7 @@ impl Contact { ) -> Result> { let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); let mut add_self = false; @@ -600,10 +637,12 @@ impl Contact { .map(|s| s.as_ref().to_string()) .unwrap_or_default() ); - context + + let mut rows = context .sql - .query_map( - "SELECT c.id FROM contacts c \ + .fetch( + sqlx::query( + "SELECT c.id FROM contacts c \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE c.addr!=?1 \ AND c.id>?2 \ @@ -612,27 +651,23 @@ impl Contact { AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \ AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \ ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;", - paramsv![ - self_addr, - DC_CONTACT_ID_LAST_SPECIAL as i32, - Origin::IncomingReplyTo, - s3str_like_cmd, - s3str_like_cmd, - if flag_verified_only { 0i32 } else { 1i32 }, - ], - |row| row.get::<_, i32>(0), - |ids| { - for id in ids { - ret.push(id? as u32); - } - Ok(()) - }, + ) + .bind(&self_addr) + .bind(DC_CONTACT_ID_LAST_SPECIAL) + .bind(Origin::IncomingReplyTo) + .bind(&s3str_like_cmd) + .bind(&s3str_like_cmd) + .bind(if flag_verified_only { 0i32 } else { 1i32 }), ) - .await?; + .await? + .map(|row| row?.try_get(0)); + while let Some(id) = rows.next().await { + ret.push(id?); + } let self_name = context .get_config(Config::Displayname) - .await + .await? .unwrap_or_default(); let self_name2 = stock_str::self_msg(context); @@ -649,25 +684,27 @@ impl Contact { } else { add_self = true; - context + let mut rows = context .sql - .query_map( - "SELECT id FROM contacts + .fetch( + sqlx::query( + "SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;", - paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100], - |row| row.get::<_, i32>(0), - |ids| { - for id in ids { - ret.push(id? as u32); - } - Ok(()) - }, + ) + .bind(self_addr) + .bind(DC_CONTACT_ID_LAST_SPECIAL) + .bind(Origin::IncomingReplyTo), ) - .await?; + .await? + .map(|row| row?.try_get(0)); + + while let Some(id) = rows.next().await { + ret.push(id?); + } } if flag_add_self && add_self { @@ -683,41 +720,55 @@ impl Contact { // from the users perspective, // there is not much difference in an email- and a mailinglist-address) async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> { - let blocked_mailinglists = context + let mut rows = context .sql - .query_map( - "SELECT name, grpid FROM chats WHERE type=? AND blocked=?;", - paramsv![Chattype::Mailinglist, Blocked::Manually], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), - |rows| { - rows.collect::, _>>() - .map_err(Into::into) - }, + .fetch( + sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;") + .bind(Chattype::Mailinglist) + .bind(Blocked::Manually), ) .await?; - for (name, grpid) in blocked_mailinglists { + + while let Some(row) = rows.next().await { + let row = row?; + let name = row.try_get::(0)?; + let grpid = row.try_get::(1)?; + if !context .sql - .exists("SELECT id FROM contacts WHERE addr=?;", paramsv![grpid]) + .exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid)) .await? { context .sql - .execute("INSERT INTO contacts (addr) VALUES (?);", paramsv![grpid]) + .execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid)) .await?; } // always do an update in case the blocking is reset or name is changed context .sql .execute( - "UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;", - paramsv![name, Origin::MailinglistAddress, grpid], + sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;") + .bind(name) + .bind(Origin::MailinglistAddress) + .bind(&grpid), ) .await?; } Ok(()) } + pub async fn get_blocked_cnt(context: &Context) -> Result { + let count = context + .sql + .count( + sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0") + .bind(DC_CONTACT_ID_LAST_SPECIAL), + ) + .await?; + Ok(count as usize) + } + /// Get blocked contacts. pub async fn get_all_blocked(context: &Context) -> Result> { if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await { @@ -727,19 +778,19 @@ impl Contact { ); } - let ret = context + let list = context .sql - .query_map( + .fetch( + sqlx::query( "SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;", - paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], - |row| row.get::<_, u32>(0), - |ids| { - ids.collect::, _>>() - .map_err(Into::into) - }, + ).bind(DC_CONTACT_ID_LAST_SPECIAL) ) + .await? + .map(|row| row?.try_get::(0)) + .collect::>>() .await?; - Ok(ret) + + Ok(list) } /// Returns a textual summary of the encryption state for the contact. @@ -755,7 +806,7 @@ impl Contact { let mut ret = String::new(); if let Ok(contact) = Contact::load_from_db(context, contact_id).await { - let loginparam = LoginParam::from_database(context, "configured_").await; + let loginparam = LoginParam::from_database(context, "configured_").await?; let peerstate = Peerstate::from_addr(context, &contact.addr).await?; if let Some(peerstate) = peerstate.filter(|peerstate| { @@ -822,26 +873,23 @@ impl Contact { "Can not delete special contact" ); - let count_contacts: i32 = context + let count_contacts = context .sql - .query_get_value( - context, - "SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;", - paramsv![contact_id as i32], + .count( + sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;") + .bind(contact_id), ) - .await - .unwrap_or_default(); + .await?; - let count_msgs: i32 = if count_contacts > 0 { + let count_msgs = if count_contacts > 0 { context .sql - .query_get_value( - context, - "SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;", - paramsv![contact_id as i32, contact_id as i32], + .count( + sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;") + .bind(contact_id) + .bind(contact_id), ) - .await - .unwrap_or_default() + .await? } else { 0 }; @@ -849,10 +897,7 @@ impl Contact { if count_msgs == 0 { match context .sql - .execute( - "DELETE FROM contacts WHERE id=?;", - paramsv![contact_id as i32], - ) + .execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32)) .await { Ok(_) => { @@ -889,8 +934,9 @@ impl Contact { context .sql .execute( - "UPDATE contacts SET param=? WHERE id=?", - paramsv![self.param.to_string(), self.id as i32], + sqlx::query("UPDATE contacts SET param=? WHERE id=?") + .bind(self.param.to_string()) + .bind(self.id as i32), ) .await?; Ok(()) @@ -901,8 +947,9 @@ impl Contact { context .sql .execute( - "UPDATE contacts SET status=? WHERE id=?", - paramsv![self.status, self.id as i32], + sqlx::query("UPDATE contacts SET status=? WHERE id=?") + .bind(&self.status) + .bind(self.id as i32), ) .await?; Ok(()) @@ -967,17 +1014,17 @@ impl Contact { /// Get the contact's profile image. /// This is the image set by each remote user on their own /// using dc_set_config(context, "selfavatar", image). - pub async fn get_profile_image(&self, context: &Context) -> Option { + pub async fn get_profile_image(&self, context: &Context) -> Result> { if self.id == DC_CONTACT_ID_SELF { - if let Some(p) = context.get_config(Config::Selfavatar).await { - return Some(PathBuf::from(p)); + if let Some(p) = context.get_config(Config::Selfavatar).await? { + return Ok(Some(PathBuf::from(p))); } } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { - return Some(dc_get_abs_path(context, image_rel)); + return Ok(Some(dc_get_abs_path(context, image_rel))); } } - None + Ok(None) } /// Get a color for the contact. @@ -1065,20 +1112,19 @@ impl Contact { false } - pub async fn get_real_cnt(context: &Context) -> usize { + pub async fn get_real_cnt(context: &Context) -> Result { if !context.sql.is_open().await { - return 0; + return Ok(0); } - context + let count = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) FROM contacts WHERE id>?;", - paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], + .count( + sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;") + .bind(DC_CONTACT_ID_LAST_SPECIAL), ) - .await - .unwrap_or_default() as usize + .await?; + Ok(count) } pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool { @@ -1088,10 +1134,7 @@ impl Contact { context .sql - .exists( - "SELECT id FROM contacts WHERE id=?;", - paramsv![contact_id as i32], - ) + .exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id)) .await .unwrap_or_default() } @@ -1100,8 +1143,10 @@ impl Contact { context .sql .execute( - "UPDATE contacts SET origin=? WHERE id=? AND origin Result { let self_addr = self .get_config(Config::ConfiguredAddr) - .await + .await? .ok_or_else(|| format_err!("Not configured"))?; Ok(addr_cmp(self_addr, addr)) diff --git a/src/context.rs b/src/context.rs index d83506551..de66f5bf6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,12 +6,14 @@ use std::ops::Deref; use std::time::{Instant, SystemTime}; use anyhow::{bail, ensure, Result}; +use async_std::prelude::*; use async_std::{ channel::{self, Receiver, Sender}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, task, }; +use sqlx::Row; use crate::chat::{get_chat_cnt, ChatId}; use crate::config::Config; @@ -89,8 +91,9 @@ pub struct RunningState { pub fn get_info() -> BTreeMap<&'static str, String> { let mut res = BTreeMap::new(); res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR)); - res.insert("sqlite_version", rusqlite::version().to_string()); + res.insert("sqlite_version", crate::sql::version().to_string()); res.insert("arch", (std::mem::size_of::() * 8).to_string()); + res.insert("num_cpus", num_cpus::get().to_string()); res.insert("level", "awesome".into()); res } @@ -270,68 +273,62 @@ impl Context { * UI chat/message related API ******************************************************************************/ - pub async fn get_info(&self) -> BTreeMap<&'static str, String> { + pub async fn get_info(&self) -> Result> { let unset = "0"; - let l = LoginParam::from_database(self, "").await; - let l2 = LoginParam::from_database(self, "configured_").await; - let displayname = self.get_config(Config::Displayname).await; - let chats = get_chat_cnt(self).await as usize; + let l = LoginParam::from_database(self, "").await?; + let l2 = LoginParam::from_database(self, "configured_").await?; + let displayname = self.get_config(Config::Displayname).await?; + let chats = get_chat_cnt(self).await? as usize; let real_msgs = message::get_real_msg_cnt(self).await as usize; let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize; - let contacts = Contact::get_real_cnt(self).await as usize; - let is_configured = self.get_config_int(Config::Configured).await; + let contacts = Contact::get_real_cnt(self).await? as usize; + let is_configured = self.get_config_int(Config::Configured).await?; let dbversion = self .sql - .get_raw_config_int(self, "dbversion") - .await + .get_raw_config_int("dbversion") + .await? .unwrap_or_default(); let journal_mode = self .sql - .query_get_value(self, "PRAGMA journal_mode;", paramsv![]) - .await + .query_get_value("PRAGMA journal_mode;") + .await? .unwrap_or_else(|| "unknown".to_string()); - let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await; - let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await; - let bcc_self = self.get_config_int(Config::BccSelf).await; + let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?; + let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?; + let bcc_self = self.get_config_int(Config::BccSelf).await?; - let prv_key_cnt: Option = self - .sql - .query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![]) - .await; + let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;").await?; - let pub_key_cnt: Option = self - .sql - .query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![]) - .await; + let pub_key_cnt = self.sql.count("SELECT COUNT(*) FROM acpeerstates;").await?; let fingerprint_str = match SignedPublicKey::load_self(self).await { Ok(key) => key.fingerprint().hex(), Err(err) => format!("", err), }; - let inbox_watch = self.get_config_int(Config::InboxWatch).await; - let sentbox_watch = self.get_config_int(Config::SentboxWatch).await; - let mvbox_watch = self.get_config_int(Config::MvboxWatch).await; - let mvbox_move = self.get_config_int(Config::MvboxMove).await; - let sentbox_move = self.get_config_int(Config::SentboxMove).await; + let inbox_watch = self.get_config_int(Config::InboxWatch).await?; + let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?; + let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?; + let mvbox_move = self.get_config_int(Config::MvboxMove).await?; + let sentbox_move = self.get_config_int(Config::SentboxMove).await?; let folders_configured = self .sql - .get_raw_config_int(self, "folders_configured") - .await + .get_raw_config_int("folders_configured") + .await? .unwrap_or_default(); let configured_sentbox_folder = self .get_config(Config::ConfiguredSentboxFolder) - .await + .await? .unwrap_or_else(|| "".to_string()); let configured_mvbox_folder = self .get_config(Config::ConfiguredMvboxFolder) - .await + .await? .unwrap_or_else(|| "".to_string()); let mut res = get_info(); // insert values - res.insert("bot", self.get_config_int(Config::Bot).await.to_string()); + res.insert("bot", self.get_config_int(Config::Bot).await?.to_string()); res.insert("number_of_chats", chats.to_string()); res.insert("number_of_chat_messages", real_msgs.to_string()); res.insert("messages_in_contact_requests", deaddrop_msgs.to_string()); @@ -344,7 +341,7 @@ impl Context { res.insert( "selfavatar", self.get_config(Config::Selfavatar) - .await + .await? .unwrap_or_else(|| "".to_string()), ); res.insert("is_configured", is_configured.to_string()); @@ -353,12 +350,12 @@ impl Context { res.insert( "fetch_existing_msgs", self.get_config_int(Config::FetchExistingMsgs) - .await + .await? .to_string(), ); res.insert( "show_emails", - self.get_config_int(Config::ShowEmails).await.to_string(), + self.get_config_int(Config::ShowEmails).await?.to_string(), ); res.insert("inbox_watch", inbox_watch.to_string()); res.insert("sentbox_watch", sentbox_watch.to_string()); @@ -372,57 +369,51 @@ impl Context { res.insert("e2ee_enabled", e2ee_enabled.to_string()); res.insert( "key_gen_type", - self.get_config_int(Config::KeyGenType).await.to_string(), + self.get_config_int(Config::KeyGenType).await?.to_string(), ); res.insert("bcc_self", bcc_self.to_string()); - res.insert( - "private_key_count", - prv_key_cnt.unwrap_or_default().to_string(), - ); - res.insert( - "public_key_count", - pub_key_cnt.unwrap_or_default().to_string(), - ); + res.insert("private_key_count", prv_key_cnt.to_string()); + res.insert("public_key_count", pub_key_cnt.to_string()); res.insert("fingerprint", fingerprint_str); res.insert( "webrtc_instance", self.get_config(Config::WebrtcInstance) - .await + .await? .unwrap_or_else(|| "".to_string()), ); res.insert( "media_quality", - self.get_config_int(Config::MediaQuality).await.to_string(), + self.get_config_int(Config::MediaQuality).await?.to_string(), ); res.insert( "delete_device_after", self.get_config_int(Config::DeleteDeviceAfter) - .await + .await? .to_string(), ); res.insert( "delete_server_after", self.get_config_int(Config::DeleteServerAfter) - .await + .await? .to_string(), ); res.insert( "last_housekeeping", self.get_config_int(Config::LastHousekeeping) - .await + .await? .to_string(), ); res.insert( "scan_all_folders_debounce_secs", self.get_config_int(Config::ScanAllFoldersDebounceSecs) - .await + .await? .to_string(), ); let elapsed = self.creation_time.elapsed(); res.insert("uptime", duration_to_str(elapsed.unwrap_or_default())); - res + Ok(res) } /// Get a list of fresh, unmuted messages in any chat but deaddrop. @@ -432,10 +423,10 @@ impl Context { /// Moreover, the number of returned messages /// can be used for a badge counter on the app icon. pub async fn get_fresh_msgs(&self) -> Result> { - let ret = self + let list = self .sql - .query_map( - concat!( + .fetch( + sqlx::query(concat!( "SELECT m.id", " FROM msgs m", " LEFT JOIN contacts ct", @@ -449,51 +440,38 @@ impl Context { " AND c.blocked=0", " AND NOT(c.muted_until=-1 OR c.muted_until>?)", " ORDER BY m.timestamp DESC,m.id DESC;" - ), - paramsv![MessageState::InFresh, time()], - |row| row.get::<_, MsgId>(0), - |rows| { - let mut ret = Vec::new(); - for row in rows { - ret.push(row?); - } - Ok(ret) - }, + )) + .bind(MessageState::InFresh) + .bind(time()), ) + .await? + .map(|row| row?.try_get("id")) + .collect::>() .await?; - Ok(ret) + Ok(list) } /// Searches for messages containing the query string. /// /// If `chat_id` is provided this searches only for messages in this chat, if `chat_id` /// is `None` this searches messages from all chats. - pub async fn search_msgs(&self, chat_id: Option, query: impl AsRef) -> Vec { + pub async fn search_msgs( + &self, + chat_id: Option, + query: impl AsRef, + ) -> Result> { let real_query = query.as_ref().trim(); if real_query.is_empty() { - return Vec::new(); + return Ok(Vec::new()); } let str_like_in_text = format!("%{}%", real_query); let str_like_beg = format!("{}%", real_query); - let do_query = |query, params| { - self.sql.query_map( - query, - params, - |row| row.get::<_, MsgId>("id"), - |rows| { - let mut ret = Vec::new(); - for id in rows { - ret.push(id?); - } - Ok(ret) - }, - ) - }; - - if let Some(chat_id) = chat_id { - do_query( - "SELECT m.id AS id, m.timestamp AS timestamp + let list = if let Some(chat_id) = chat_id { + self.sql + .fetch( + sqlx::query( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -502,13 +480,24 @@ impl Context { AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;", - paramsv![chat_id, str_like_in_text, str_like_beg], - ) - .await - .unwrap_or_default() + ) + .bind(chat_id) + .bind(str_like_in_text) + .bind(str_like_beg), + ) + .await? + .map(|row| { + let row = row?; + let id = row.try_get::("id")?; + Ok(id) + }) + .collect::>>() + .await? } else { - do_query( - "SELECT m.id AS id, m.timestamp AS timestamp + self.sql + .fetch( + sqlx::query( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -520,31 +509,45 @@ impl Context { AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;", - paramsv![str_like_in_text, str_like_beg], - ) - .await - .unwrap_or_default() - } + ) + .bind(str_like_in_text) + .bind(str_like_beg), + ) + .await? + .map(|row| { + let row = row?; + let id = row.try_get::("id")?; + Ok(id) + }) + .collect::>>() + .await? + }; + + Ok(list) } - pub async fn is_inbox(&self, folder_name: impl AsRef) -> bool { - self.get_config(Config::ConfiguredInboxFolder).await - == Some(folder_name.as_ref().to_string()) + pub async fn is_inbox(&self, folder_name: impl AsRef) -> Result { + let inbox = self.get_config(Config::ConfiguredInboxFolder).await?; + Ok(inbox == Some(folder_name.as_ref().to_string())) } - pub async fn is_sentbox(&self, folder_name: impl AsRef) -> bool { - self.get_config(Config::ConfiguredSentboxFolder).await - == Some(folder_name.as_ref().to_string()) + pub async fn is_sentbox(&self, folder_name: impl AsRef) -> Result { + let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?; + + Ok(sentbox == Some(folder_name.as_ref().to_string())) } - pub async fn is_mvbox(&self, folder_name: impl AsRef) -> bool { - self.get_config(Config::ConfiguredMvboxFolder).await - == Some(folder_name.as_ref().to_string()) + pub async fn is_mvbox(&self, folder_name: impl AsRef) -> Result { + let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?; + + Ok(mvbox == Some(folder_name.as_ref().to_string())) } - pub async fn is_spam_folder(&self, folder_name: impl AsRef) -> bool { - self.get_config(Config::ConfiguredSpamFolder).await - == Some(folder_name.as_ref().to_string()) + pub async fn is_spam_folder(&self, folder_name: impl AsRef) -> Result { + let is_spam = self.get_config(Config::ConfiguredSpamFolder).await? + == Some(folder_name.as_ref().to_string()); + + Ok(is_spam) } pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf { @@ -620,7 +623,7 @@ mod tests { } async fn receive_msg(t: &TestContext, chat: &Chat) { - let members = get_chat_contacts(t, chat.id).await; + let members = get_chat_contacts(t, chat.id).await.unwrap(); let contact = Contact::load_from_db(t, *members.first().unwrap()) .await .unwrap(); @@ -651,43 +654,49 @@ mod tests { assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); receive_msg(&t, &bob).await; - assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1); - assert_eq!(bob.id.get_fresh_msg_cnt(&t).await, 1); + assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1); + assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); receive_msg(&t, &claire).await; receive_msg(&t, &claire).await; - assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 2); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2); + assert_eq!( + get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(), + 2 + ); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3); receive_msg(&t, &dave).await; receive_msg(&t, &dave).await; receive_msg(&t, &dave).await; - assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.len(), 3); - assert_eq!(dave.id.get_fresh_msg_cnt(&t).await, 3); + assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3); + assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // mute one of the chats set_muted(&t, claire.id, MuteDuration::Forever) .await .unwrap(); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted // receive more messages receive_msg(&t, &bob).await; receive_msg(&t, &claire).await; receive_msg(&t, &dave).await; - assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 3); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3); + assert_eq!( + get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(), + 3 + ); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted // unmute claire again set_muted(&t, claire.id, MuteDuration::NotMuted) .await .unwrap(); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again } @@ -696,7 +705,7 @@ mod tests { let t = TestContext::new_alice().await; let bob = t.create_chat_with_contact("", "bob@g.it").await; receive_msg(&t, &bob).await; - assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1); + assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1); // chat is unmuted by default, here and in the following assert(), // we check mainly that the SQL-statements in is_muted() and get_fresh_msgs() @@ -720,8 +729,9 @@ mod tests { // we need to modify the database directly t.sql .execute( - "UPDATE chats SET muted_until=? WHERE id=?;", - paramsv![time() - 3600, bob.id], + sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;") + .bind(time() - 3600) + .bind(bob.id), ) .await .unwrap(); @@ -738,10 +748,7 @@ mod tests { // to test get_fresh_msgs() with invalid mute_until (everything < -1), // that results in "muted forever" by definition. t.sql - .execute( - "UPDATE chats SET muted_until=-2 WHERE id=?;", - paramsv![bob.id], - ) + .execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id)) .await .unwrap(); let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); @@ -811,7 +818,7 @@ mod tests { async fn test_get_info() { let t = TestContext::new().await; - let info = t.get_info().await; + let info = t.get_info().await.unwrap(); assert!(info.get("database_dir").is_some()); } @@ -851,7 +858,7 @@ mod tests { "smtp_certificate_checks", ]; let t = TestContext::new().await; - let info = t.get_info().await; + let info = t.get_info().await.unwrap(); for key in Config::iter() { let key: String = key.to_string(); if !skip_from_get_info.contains(&&*key) diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 550c22a73..e6ceb87d6 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -1,10 +1,14 @@ +use std::convert::TryFrom; + use anyhow::{bail, ensure, format_err, Result}; +use async_std::prelude::*; use itertools::join; use mailparse::SingleInfo; use num_traits::FromPrimitive; use once_cell::sync::Lazy; use regex::Regex; use sha2::{Digest, Sha256}; +use sqlx::Row; use crate::chat::{self, Chat, ChatId, ProtectionStatus}; use crate::config::Config; @@ -31,7 +35,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on use crate::stock_str; use crate::{contact, location}; -// IndexSet is like HashSet but maintains order of insertion +// IndexSet is like HashSet but maintains order of insertion. type ContactIds = indexmap::IndexSet; #[derive(Debug, PartialEq, Eq)] @@ -252,7 +256,7 @@ pub(crate) async fn dc_receive_imf_inner( } // Get user-configured server deletion - let delete_server_after = context.get_config_delete_server_after().await; + 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) { @@ -417,7 +421,7 @@ async 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).await).unwrap_or_default(); + ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && is_dc_message == MessengerMessage::No { @@ -425,7 +429,7 @@ async fn add_parts( match show_emails { ShowEmails::Off => { info!(context, "Classical email not shown (TRASH)"); - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; allow_creation = false; } ShowEmails::AcceptedContacts => allow_creation = false, @@ -481,7 +485,7 @@ async fn add_parts( // 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 if chat_id.is_unset() && mime_parser.failure_report.is_some() { - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; info!(context, "Message belongs to an NDN (TRASH)",); } @@ -603,7 +607,7 @@ async fn add_parts( } if chat_id.is_unset() { // maybe from_id is null or sth. else is suspicious, move message to trash - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; info!(context, "No chat id for incoming msg (TRASH)") } @@ -651,13 +655,13 @@ async fn add_parts( } } - if !context.is_sentbox(&server_folder).await + if !context.is_sentbox(&server_folder).await? && mime_parser.get(HeaderDef::Received).is_none() { // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them // So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email. info!(context, "Email is probably just a draft (TRASH)"); - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; allow_creation = false; } @@ -723,13 +727,13 @@ async fn add_parts( } } if chat_id.is_unset() { - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; info!(context, "No chat id for outgoing message (TRASH)") } } if fetching_existing_messages && mime_parser.decrypting_failed { - *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + *chat_id = DC_CHAT_ID_TRASH; // We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats. info!(context, "Existing non-decipherable message. (TRASH)"); } @@ -810,8 +814,7 @@ async fn add_parts( }; if chat.is_protected() || new_status.is_some() { - if let Err(err) = - check_verified_properties(context, mime_parser, from_id as u32, to_ids).await + if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { warn!(context, "verification problem: {}", err); let s = format!("{}. See 'Info' for more details", err); @@ -830,7 +833,9 @@ async fn add_parts( } set_better_msg( mime_parser, - context.stock_protection_msg(new_status, from_id).await, + context + .stock_protection_msg(new_status, from_id as u32) + .await, ); } } @@ -841,7 +846,7 @@ async fn add_parts( // however, we cannot do this earlier as we need from_id to be set let in_fresh = state == MessageState::InFresh; let rcvd_timestamp = time(); - let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await; + let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await?; // Ensure replies to messages are sorted after the parent message. // @@ -860,7 +865,7 @@ async fn add_parts( // 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).await; + 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(); } @@ -877,8 +882,7 @@ async fn add_parts( let subject = mime_parser.get_subject().unwrap_or_default(); - let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new()); - let server_folder = server_folder.as_ref().to_string(); + let server_folder = server_folder.as_ref(); let is_system_message = mime_parser.is_system_message; // if indicated by the parser, @@ -889,140 +893,133 @@ async fn add_parts( let mut save_mime_modified = mime_parser.is_mime_modified; let mime_headers = if save_mime_headers || save_mime_modified { - if mime_parser.was_encrypted() { - Some(String::from_utf8_lossy(&mime_parser.decoded_data).to_string()) + if mime_parser.was_encrypted() && !mime_parser.decoded_data.is_empty() { + String::from_utf8_lossy(&mime_parser.decoded_data) } else { - Some(String::from_utf8_lossy(imf_raw).to_string()) + String::from_utf8_lossy(imf_raw) } } else { - None + "".into() }; - let sent_timestamp = *sent_timestamp; - let is_hidden = *hidden; - let chat_id = *chat_id; + for part in &mut mime_parser.parts { + let mut txt_raw = "".to_string(); - // TODO: can this clone be avoided? - let rfc724_mid = rfc724_mid.to_string(); + let is_location_kml = + location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty()); - 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, subject, txt_raw, param, \ - bytes, hidden, mime_headers, mime_in_reply_to, mime_references, mime_modified, \ - error, ephemeral_timer, ephemeral_timestamp) \ - VALUES (?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?);", - )?; - - let is_location_kml = location_kml_is - && icnt == 1 - && (part.msg == "-location-" || part.msg.is_empty()); - - if is_mdn || is_location_kml { - is_hidden = true; - if incoming { - state = MessageState::InSeen; // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message - } - } - - let mime_modified = save_mime_modified && !part.msg.is_empty(); - if mime_modified { - // Avoid setting mime_modified for more than one part. - save_mime_modified = false; - } - - if part.typ == Viewtype::Text { - let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); - txt_raw = format!("{}\n\n{}", subject, msg_raw); - } - if is_system_message != SystemMessage::Unknown { - part.param.set_int(Param::Cmd, is_system_message as i32); - } - - let ephemeral_timestamp = if in_fresh { - 0 - } else { - match ephemeral_timer { - EphemeralTimer::Disabled => 0, - EphemeralTimer::Enabled { duration } => { - rcvd_timestamp + i64::from(duration) - } - } - }; - - // If you change which information is skipped if the message is trashed, - // also change `MsgId::trash()` and `delete_expired_messages()` - let trash = chat_id.is_trash(); - - stmt.execute(paramsv![ - rfc724_mid, - server_folder, - server_uid as i32, - chat_id, - if trash { 0 } else { from_id as i32 }, - if trash { 0 } else { to_id as i32 }, - sort_timestamp, - sent_timestamp, - rcvd_timestamp, - part.typ, - state, - is_dc_message, - if trash { "" } else { &part.msg }, - if trash { "" } else { &subject }, - // txt_raw might contain invalid utf8 - if trash { "" } else { &txt_raw }, - if trash { - "".to_string() - } else { - part.param.to_string() - }, - part.bytes as isize, - is_hidden, - if (save_mime_headers || mime_modified) && !trash { - mime_headers.clone() - } else { - None - }, - mime_in_reply_to, - mime_references, - mime_modified, - part.error.take().unwrap_or_default(), - ephemeral_timer, - ephemeral_timestamp - ])?; - - drop(stmt); - ids.push(MsgId::new(crate::sql::get_rowid( - &mut conn, - "msgs", - "rfc724_mid", - &rfc724_mid, - )?)); + if is_mdn || is_location_kml { + *hidden = true; + if incoming { + // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message + state = MessageState::InSeen; } - Ok((parts, ids, is_hidden)) - }) - .await?; + } - if let Some(id) = ids.iter().last() { - *insert_msg_id = *id; + let mime_modified = save_mime_modified && !part.msg.is_empty(); + if mime_modified { + // Avoid setting mime_modified for more than one part. + save_mime_modified = false; + } + + if part.typ == Viewtype::Text { + let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); + txt_raw = format!("{}\n\n{}", subject, msg_raw); + } + if is_system_message != SystemMessage::Unknown { + part.param.set_int(Param::Cmd, is_system_message as i32); + } + + let ephemeral_timestamp = if in_fresh { + 0 + } else { + match ephemeral_timer { + EphemeralTimer::Disabled => 0, + EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration), + } + }; + + // If you change which information is skipped if the message is trashed, + // also change `MsgId::trash()` and `delete_expired_messages()` + let trash = chat_id.is_trash(); + + context + .sql + .execute( + sqlx::query( + r#" +INSERT INTO msgs + ( + rfc724_mid, server_folder, server_uid, chat_id, + from_id, to_id, timestamp, timestamp_sent, + timestamp_rcvd, type, state, msgrmsg, + txt, subject, txt_raw, param, + bytes, hidden, mime_headers, mime_in_reply_to, + mime_references, mime_modified, error, ephemeral_timer, + ephemeral_timestamp + ) + VALUES ( + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ? + ); +"#, + ) + .bind(rfc724_mid) + .bind(server_folder) + .bind(server_uid as i32) + .bind(*chat_id) + .bind(if trash { 0 } else { from_id as i32 }) + .bind(if trash { 0 } else { to_id as i32 }) + .bind(sort_timestamp) + .bind(*sent_timestamp) + .bind(rcvd_timestamp) + .bind(part.typ) + .bind(state) + .bind(is_dc_message) + .bind(if trash { "" } else { &part.msg }) + .bind(if trash { "" } else { &subject }) + // txt_raw might contain invalid utf8 + .bind(if trash { "" } else { &txt_raw }) + .bind(if trash { + "".to_string() + } else { + part.param.to_string() + }) + .bind(part.bytes as i64) + .bind(*hidden) + .bind(if (save_mime_headers || mime_modified) && !trash { + mime_headers.to_string() + } else { + "".to_string() + }) + .bind(&mime_in_reply_to) + .bind(&mime_references) + .bind(&mime_modified) + .bind(part.error.take().unwrap_or_default()) + .bind(ephemeral_timer) + .bind(ephemeral_timestamp), + ) + .await?; + let msg_id = MsgId::new(u32::try_from( + context + .sql + .get_rowid("msgs", "rfc724_mid", &rfc724_mid) + .await?, + )?); + + created_db_entries.push((*chat_id, msg_id)); + *insert_msg_id = msg_id; } - if !is_hidden { + if !*hidden { chat_id.unarchive(context).await?; } - *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, @@ -1030,7 +1027,7 @@ async fn add_parts( // new outgoing message from another device marks the chat as noticed. if !incoming && !*hidden && !chat_id.is_special() { - chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?; + chat::marknoticed_chat_if_older_than(context, *chat_id, sort_timestamp).await?; } // check event to send @@ -1060,7 +1057,7 @@ async fn add_parts( Ok(()) } if !is_mdn { - update_last_subject(context, chat_id, mime_parser) + update_last_subject(context, *chat_id, mime_parser) .await .unwrap_or_else(|e| { warn!( @@ -1139,7 +1136,7 @@ async fn calc_sort_timestamp( message_timestamp: i64, chat_id: ChatId, is_fresh_msg: bool, -) -> i64 { +) -> Result { let mut sort_timestamp = message_timestamp; // get newest non fresh message for this chat @@ -1148,11 +1145,11 @@ async fn calc_sort_timestamp( let last_msg_time: Option = context .sql .query_get_value( - context, - "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?", - paramsv![chat_id, MessageState::InFresh], + sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?") + .bind(chat_id) + .bind(MessageState::InFresh), ) - .await; + .await?; if let Some(last_msg_time) = last_msg_time { if last_msg_time > sort_timestamp { @@ -1165,7 +1162,7 @@ async fn calc_sort_timestamp( sort_timestamp = dc_create_smeared_timestamp(context).await; } - sort_timestamp + Ok(sort_timestamp) } /// This function tries to extract the group-id from the message and returns the @@ -1197,7 +1194,7 @@ async fn create_or_lookup_group( let mut better_msg: String = From::from(""); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { - better_msg = stock_str::msg_location_enabled_by(context, from_id).await; + better_msg = stock_str::msg_location_enabled_by(context, from_id as u32).await; set_better_msg(mime_parser, &better_msg); } @@ -1205,11 +1202,11 @@ async fn create_or_lookup_group( grpid } else { let mut member_ids: Vec = to_ids.iter().copied().collect(); - if !member_ids.contains(&from_id) { - member_ids.push(from_id); + if !member_ids.contains(&(from_id as u32)) { + member_ids.push(from_id as u32); } - if !member_ids.contains(&DC_CONTACT_ID_SELF) { - member_ids.push(DC_CONTACT_ID_SELF); + if !member_ids.contains(&(DC_CONTACT_ID_SELF as u32)) { + member_ids.push(DC_CONTACT_ID_SELF as u32); } // Try to assign message to the same group as the parent message. @@ -1313,7 +1310,7 @@ async fn create_or_lookup_group( let (mut chat_id, _, _blocked) = chat::get_chat_id_by_grpid(context, &grpid) .await .unwrap_or((ChatId::new(0), false, Blocked::Not)); - if !chat_id.is_unset() && !chat::is_contact_in_chat(context, chat_id, from_id as u32).await { + if !chat_id.is_unset() && !chat::is_contact_in_chat(context, chat_id, from_id).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 @@ -1330,7 +1327,7 @@ async fn create_or_lookup_group( .unwrap_or_default(); let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); if chat_id.is_unset() @@ -1345,8 +1342,7 @@ async fn create_or_lookup_group( { // group does not exist but should be created let create_protected = if mime_parser.get(HeaderDef::ChatVerified).is_some() { - if let Err(err) = - check_verified_properties(context, mime_parser, from_id as u32, to_ids).await + if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { warn!(context, "verification problem: {}", err); let s = format!("{}. See 'Info' for more details", err); @@ -1370,7 +1366,19 @@ async fn create_or_lookup_group( create_blocked, create_protected, ) - .await?; + .await + .unwrap_or_else(|err| { + warn!( + context, + "Failed to create group '{}' for grpid={}: {:?}", + grpname.as_ref().unwrap(), + grpid, + err, + ); + + ChatId::new(0) + }); + chat_id_blocked = create_blocked; recreate_member_list = true; @@ -1400,7 +1408,7 @@ async fn create_or_lookup_group( // The message was decrypted successfully, but contains a late "quit" or otherwise // unwanted message. info!(context, "message belongs to unwanted group (TRASH)"); - return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked)); + return Ok((DC_CHAT_ID_TRASH, chat_id_blocked)); } } @@ -1426,8 +1434,9 @@ async fn create_or_lookup_group( if context .sql .execute( - "UPDATE chats SET name=? WHERE id=?;", - paramsv![grpname.to_string(), chat_id], + sqlx::query("UPDATE chats SET name=? WHERE id=?;") + .bind(grpname.to_string()) + .bind(chat_id), ) .await .is_ok() @@ -1464,20 +1473,17 @@ async fn create_or_lookup_group( // start from scratch. context .sql - .execute( - "DELETE FROM chats_contacts WHERE chat_id=?;", - paramsv![chat_id], - ) + .execute(sqlx::query("DELETE FROM chats_contacts WHERE chat_id=?;").bind(chat_id)) .await .ok(); 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).await + && !Contact::addr_equals_contact(context, &self_addr, from_id).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).await; + chat::add_to_chat_contacts_table(context, chat_id, from_id).await; } for &to_id in to_ids.iter() { info!(context, "adding to={:?} to chat id={}", to_id, chat_id); @@ -1650,6 +1656,7 @@ async fn create_adhoc_group( context, "not creating ad-hoc group for mailing list message" ); + return Ok(None); } @@ -1675,7 +1682,7 @@ async fn create_adhoc_group( } // Create a new ad-hoc group. - let grpid = create_adhoc_grp_id(context, member_ids).await; + let grpid = create_adhoc_grp_id(context, member_ids).await?; // use subject as initial chat name let grpname = mime_parser @@ -1686,7 +1693,7 @@ async fn create_adhoc_group( context, Chattype::Group, &grpid, - grpname, + &grpname, create_blocked, ProtectionStatus::Unprotected, ) @@ -1709,23 +1716,22 @@ async fn create_multiuser_record( create_protected: ProtectionStatus, ) -> Result { context.sql.execute( - "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);", - paramsv![ - chattype, - grpname.as_ref(), - grpid.as_ref(), - create_blocked, - time(), - create_protected, - ], + sqlx::query( + "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);") + .bind(chattype) + .bind(grpname.as_ref()) + .bind(grpid.as_ref()) + .bind(create_blocked) + .bind(time()) + .bind(create_protected) ).await?; let row_id = context .sql - .get_rowid(context, "chats", "grpid", grpid.as_ref()) + .get_rowid("chats", "grpid", grpid.as_ref()) .await?; - let chat_id = ChatId::new(row_id); + let chat_id = ChatId::new(u32::try_from(row_id)?); info!( context, "Created group/mailinglist '{}' grpid={} as {}", @@ -1733,6 +1739,7 @@ async fn create_multiuser_record( grpid.as_ref(), chat_id ); + Ok(chat_id) } @@ -1747,38 +1754,34 @@ async fn create_multiuser_record( /// This ensures that different Delta Chat clients generate the same group ID unless some of them /// are hidden in BCC. This group ID is sent by DC in the messages sent to this chat, /// so having the same ID prevents group split. -async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String { +async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result { let member_ids_str = join(member_ids.iter().map(|x| x.to_string()), ","); let member_cs = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_else(|| "no-self".to_string()) .to_lowercase(); - let members = context - .sql - .query_map( - format!( - "SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF - member_ids_str - ), - paramsv![], - |row| row.get::<_, String>(0), - |rows| { - let mut addrs = rows.collect::, _>>()?; - addrs.sort(); - let mut acc = member_cs.clone(); - for addr in &addrs { - acc += ","; - acc += &addr.to_lowercase(); - } - Ok(acc) - }, - ) - .await - .unwrap_or(member_cs); + let q = format!( + "SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF + member_ids_str + ); - hex_hash(&members) + let mut members = member_cs; + + if let Ok(rows) = context.sql.fetch(sqlx::query(&q)).await { + let mut addrs = rows + .map(|row| row?.try_get::(0)) + .collect::>>() + .await?; + addrs.sort(); + for addr in &addrs { + members += ","; + members += &addr.to_lowercase(); + } + } + + Ok(hex_hash(&members)) } #[allow(clippy::indexing_slicing)] @@ -1843,31 +1846,26 @@ async 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 q = 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 - ), - paramsv![], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1).unwrap_or(0))), - |rows| { - rows.collect::, _>>() - .map_err(Into::into) - }, - ) - .await?; + to_ids_str + ); + + let mut rows = context.sql.fetch(sqlx::query(&q)).await?; + + while let Some(row) = rows.next().await { + let row = row?; + let to_addr: String = row.try_get(0)?; + let mut is_verified = row.try_get::(1)? != 0; - for (to_addr, _is_verified) in rows.into_iter() { info!( context, "check_verified_properties: {:?} self={:?}", to_addr, context.is_self_addr(&to_addr).await ); - let mut is_verified = _is_verified != 0; + let peerstate = Peerstate::from_addr(context, &to_addr).await?; // mark gossiped keys (if any) as verified @@ -2142,7 +2140,7 @@ mod tests { #[async_std::test] async fn test_adhoc_group_show_chats_only() { let t = TestContext::new_alice().await; - assert_eq!(t.get_config_int(Config::ShowEmails).await, 0); + assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); @@ -2213,14 +2211,27 @@ mod tests { let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); assert_eq!(chat.typ, Chattype::Single); assert_eq!(chat.name, "Bob"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.len(), 1); - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0, None).await.len(), 1); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1); + assert_eq!( + chat::get_chat_msgs(&t, chat_id, 0, None) + .await + .unwrap() + .len(), + 1 + ); // receive a non-delta-message from Bob, shows up because of the show_emails setting dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false) .await .unwrap(); - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0, None).await.len(), 2); + + assert_eq!( + chat::get_chat_msgs(&t, chat_id, 0, None) + .await + .unwrap() + .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, GRP_MAIL, "INBOX", 3, false) @@ -2234,7 +2245,7 @@ mod tests { let chat = chat::Chat::load_from_db(&t, 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, chat_id).await.len(), 3); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); } #[async_std::test] @@ -2255,7 +2266,7 @@ mod tests { let chat = chat::Chat::load_from_db(&t, 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, chat_id).await.len(), 3); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); } #[async_std::test] @@ -2277,7 +2288,13 @@ mod tests { .await .unwrap(); chat::add_contact_to_chat(&t, group_id, bob_id).await; - assert_eq!(chat::get_chat_msgs(&t, group_id, 0, None).await.len(), 0); + assert_eq!( + chat::get_chat_msgs(&t, group_id, 0, None) + .await + .unwrap() + .len(), + 0 + ); group_id .set_visibility(&t, ChatVisibility::Archived) .await @@ -2365,7 +2382,13 @@ mod tests { false, ) .await.unwrap(); - assert_eq!(chat::get_chat_msgs(&t, group_id, 0, None).await.len(), 1); + assert_eq!( + chat::get_chat_msgs(&t, group_id, 0, None) + .await + .unwrap() + .len(), + 1 + ); let msg = message::Message::load_from_db(&t, msg.id).await.unwrap(); assert_eq!(msg.state, MessageState::OutMdnRcvd); @@ -2693,7 +2716,7 @@ mod tests { assert_eq!(msg.state, MessageState::OutFailed); - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap(); let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { msg_id } else { @@ -2775,7 +2798,13 @@ mod tests { assert!(chat.is_mailing_list()); assert_eq!(chat.can_send(), false); assert_eq!(chat.name, "deltachat/deltachat-core-rust"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 1); + assert_eq!( + chat::get_chat_contacts(&t.ctx, chat_id) + .await + .unwrap() + .len(), + 1 + ); dc_receive_imf(&t.ctx, GH_MAILINGLIST2, "INBOX", 1, false) .await @@ -2847,7 +2876,7 @@ mod tests { #[async_std::test] async fn test_mailing_list_decide_block() { - let deaddrop = ChatId::new(DC_CHAT_ID_DEADDROP); + let deaddrop = DC_CHAT_ID_DEADDROP; let t = TestContext::new_alice().await; t.ctx .set_config(Config::ShowEmails, Some("2")) @@ -2868,7 +2897,9 @@ mod tests { let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // Test that the message disappeared - let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 0); dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 1, false) @@ -2878,13 +2909,15 @@ mod tests { // Test that the mailing list stays disappeared let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // Test that the message is not shown - let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 0); } #[async_std::test] async fn test_mailing_list_decide_block_then_unblock() { - let deaddrop = ChatId::new(DC_CHAT_ID_DEADDROP); + let deaddrop = DC_CHAT_ID_DEADDROP; let t = TestContext::new_alice().await; t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); @@ -2900,7 +2933,7 @@ mod tests { message::decide_on_contact_request(&t, msg.get_id(), Block).await; let blocked = Contact::get_all_blocked(&t).await.unwrap(); assert_eq!(blocked.len(), 1); - let msgs = chat::get_chat_msgs(&t, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t, deaddrop, 0, None).await.unwrap(); assert_eq!(msgs.len(), 0); // Unblock contact and check if the next message arrives in real chat @@ -2913,15 +2946,15 @@ mod tests { .unwrap(); let msg = t.get_last_msg().await; assert_ne!(msg.chat_id, deaddrop); - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap(); assert_eq!(msgs.len(), 2); - let msgs = chat::get_chat_msgs(&t, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t, deaddrop, 0, None).await.unwrap(); assert_eq!(msgs.len(), 0); } #[async_std::test] async fn test_mailing_list_decide_not_now() { - let deaddrop = ChatId::new(DC_CHAT_ID_DEADDROP); + let deaddrop = DC_CHAT_ID_DEADDROP; let t = TestContext::new_alice().await; t.ctx .set_config(Config::ShowEmails, Some("2")) @@ -2939,7 +2972,9 @@ mod tests { let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // Test that the message disappeared - let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 1); // ...but is still shown in the deaddrop dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 1, false) @@ -2948,13 +2983,15 @@ mod tests { let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); // Test that the new mailing list message is shown again - let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, deaddrop, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 2); } #[async_std::test] async fn test_mailing_list_decide_accept() { - let deaddrop = ChatId::new(DC_CHAT_ID_DEADDROP); + let deaddrop = DC_CHAT_ID_DEADDROP; let t = TestContext::new_alice().await; t.ctx .set_config(Config::ShowEmails, Some("2")) @@ -2979,7 +3016,7 @@ mod tests { .await .unwrap(); - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap(); assert_eq!(msgs.len(), 2); } @@ -3015,7 +3052,13 @@ mod tests { assert_eq!(chat.typ, Chattype::Mailinglist); assert_eq!(chat.grpid, "mylist@bar.org"); assert_eq!(chat.name, "ola"); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0, None).await.len(), 1); + assert_eq!( + chat::get_chat_msgs(&t, chat.id, 0, None) + .await + .unwrap() + .len(), + 1 + ); // receive another message with no sender name but the same address, // make sure this lands in the same chat @@ -3036,7 +3079,13 @@ mod tests { ) .await .unwrap(); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0, None).await.len(), 2); + assert_eq!( + chat::get_chat_msgs(&t, chat.id, 0, None) + .await + .unwrap() + .len(), + 2 + ); } #[async_std::test] @@ -3129,8 +3178,8 @@ mod tests { async fn test_mailing_list_with_mimepart_footer() { let t = TestContext::new_alice().await; t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - let deaddrop = ChatId::new(DC_CHAT_ID_DEADDROP); - assert_eq!(get_chat_msgs(&t, deaddrop, 0, None).await.len(), 0); + let deaddrop = DC_CHAT_ID_DEADDROP; + assert_eq!(get_chat_msgs(&t, deaddrop, 0, None).await.unwrap().len(), 0); // the mailing list message contains two top-level texts. // the second text is a footer that is added by some mailing list software @@ -3153,8 +3202,11 @@ mod tests { ); assert!(msg.has_html()); let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, deaddrop, 0, None).await.len(), 1); - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0, None).await.len(), 1); + assert_eq!(get_chat_msgs(&t, deaddrop, 0, None).await.unwrap().len(), 1); + assert_eq!( + get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap().len(), + 1 + ); assert_eq!(chat.typ, Chattype::Mailinglist); assert_eq!(chat.blocked, Blocked::Deaddrop); assert_eq!(chat.grpid, "intern.lists.abc.de"); @@ -3176,12 +3228,15 @@ mod tests { .await .unwrap(); let msg = t.get_last_msg().await; - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0, None).await.len(), 1); + assert_eq!( + get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap().len(), + 1 + ); let text = msg.text.clone().unwrap(); assert!(text.contains("content text")); assert!(!text.contains("footer text")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&t).await.unwrap(); + let html = msg.get_id().get_html(&t).await.unwrap().unwrap(); assert!(html.contains("content text")); assert!(!html.contains("footer text")); } @@ -3292,7 +3347,7 @@ YEAAAAAA!. assert_eq!(msg.viewtype, Viewtype::Image); assert!(msg.has_html()); let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, chat.id, 0, None).await.len(), 1); + assert_eq!(get_chat_msgs(&t, chat.id, 0, None).await.unwrap().len(), 1); } /// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To` @@ -3346,7 +3401,7 @@ YEAAAAAA!. assert_eq!(msg.get_text().unwrap(), "reply foo"); // Load the first message from the same chat. - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap(); let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { msg_id } else { @@ -3475,4 +3530,52 @@ YEAAAAAA!. assert_eq!(chat.typ, Chattype::Single); assert_eq!(msg.get_text().unwrap(), "private reply"); } + + #[async_std::test] + async fn test_save_mime_headers_off() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + bob.recv_msg(&alice.pop_sent_msg().await).await; + let msg = bob.get_last_msg().await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + assert!(mime.is_empty()); + Ok(()) + } + + #[async_std::test] + async fn test_save_mime_headers_on() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config_bool(Config::SaveMimeHeaders, true).await?; + let bob = TestContext::new_bob().await; + bob.set_config_bool(Config::SaveMimeHeaders, true).await?; + + // alice sends a message to bob, bob sees full mime + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + bob.recv_msg(&alice.pop_sent_msg().await).await; + let msg = bob.get_last_msg().await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + assert!(mime.contains("Received:")); + assert!(mime.contains("From:")); + + // another one, from bob to alice, that gets encrypted + let chat_bob = bob.create_chat(&alice).await; + chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; + alice.recv_msg(&bob.pop_sent_msg().await).await; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text(), Some("ho!".to_string())); + assert!(msg.get_showpadlock()); + let mime = message::get_mime_headers(&alice, msg.id).await?; + assert!(mime.contains("Received:")); + assert!(mime.contains("From:")); + Ok(()) + } } diff --git a/src/dc_tools.rs b/src/dc_tools.rs index bf00bf86e..d637ff6d1 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -632,14 +632,6 @@ impl FromStr for EmailAddress { } } -impl rusqlite::types::ToSql for EmailAddress { - fn to_sql(&self) -> rusqlite::Result { - let val = rusqlite::types::Value::Text(self.to_string()); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - /// Makes sure that a user input that is not supposed to contain newlines does not contain newlines. pub(crate) fn improve_single_line_input(input: impl AsRef) -> String { input @@ -1053,7 +1045,9 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); let device_chat_id = chats.get_chat_id(0); - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 1); // the message should be added only once a day - test that an hour later and nearly a day later @@ -1063,7 +1057,9 @@ mod tests { get_provider_update_timestamp(), ) .await; - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 1); maybe_warn_on_bad_time( @@ -1072,7 +1068,9 @@ mod tests { get_provider_update_timestamp(), ) .await; - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 1); // next day, there should be another device message @@ -1085,7 +1083,9 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); assert_eq!(device_chat_id, chats.get_chat_id(0)); - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 2); } @@ -1115,7 +1115,9 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); let device_chat_id = chats.get_chat_id(0); - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), 1); // do not repeat the warning every day ... @@ -1135,7 +1137,9 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); let device_chat_id = chats.get_chat_id(0); - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); let test_len = msgs.len(); assert!(test_len == 1 || test_len == 2); @@ -1150,7 +1154,9 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); let device_chat_id = chats.get_chat_id(0); - let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); assert_eq!(msgs.len(), test_len + 1); } } diff --git a/src/e2ee.rs b/src/e2ee.rs index 51a5f7d6a..2665b1142 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -26,9 +26,9 @@ pub struct EncryptHelper { impl EncryptHelper { pub async fn new(context: &Context) -> Result { let prefer_encrypt = - EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await) + EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?) .unwrap_or_default(); - let addr = match context.get_config(Config::ConfiguredAddr).await { + let addr = match context.get_config(Config::ConfiguredAddr).await? { None => { bail!("addr not configured!"); } @@ -329,7 +329,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool { pub async fn ensure_secret_key_exists(context: &Context) -> Result { let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .ok_or_else(|| { format_err!(concat!( "Failed to get self address, ", diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 429409ffc..d5e35b0bc 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -61,9 +61,10 @@ use std::num::ParseIntError; use std::str::FromStr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use anyhow::{ensure, Error}; +use anyhow::{ensure, Context as _, Error}; use async_std::task; use serde::{Deserialize, Serialize}; +use sqlx::Row; use crate::chat::{lookup_by_contact_id, send_msg, ChatId}; use crate::constants::{ @@ -120,28 +121,41 @@ impl FromStr for Timer { } } -impl rusqlite::types::ToSql for Timer { - fn to_sql(&self) -> rusqlite::Result { - let val = rusqlite::types::Value::Integer(match self { - Self::Disabled => 0, - Self::Enabled { duration } => i64::from(*duration), - }); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) +impl sqlx::Type for Timer { + fn type_info() -> sqlx::sqlite::SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { + >::compatible(ty) } } -impl rusqlite::types::FromSql for Timer { - fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { - i64::column_result(value).and_then(|value| { - if value == 0 { - Ok(Self::Disabled) - } else if let Ok(duration) = u32::try_from(value) { - Ok(Self::Enabled { duration }) - } else { - Err(rusqlite::types::FromSqlError::OutOfRange(value)) - } - }) +impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer { + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> sqlx::encode::IsNull { + args.push(sqlx::sqlite::SqliteArgumentValue::Int64( + self.to_u32() as i64 + )); + + sqlx::encode::IsNull::No + } +} + +impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer { + fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let value: i64 = sqlx::Decode::decode(value)?; + if value == 0 { + Ok(Self::Disabled) + } else if let Ok(duration) = u32::try_from(value) { + Ok(Self::Enabled { duration }) + } else { + Err(Box::new(sqlx::Error::Decode(Box::new( + crate::error::OutOfRangeError, + )))) + } } } @@ -150,9 +164,8 @@ impl ChatId { pub async fn get_ephemeral_timer(self, context: &Context) -> Result { let timer = context .sql - .query_get_value_result( - "SELECT ephemeral_timer FROM chats WHERE id=?;", - paramsv![self], + .query_get_value( + sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self), ) .await?; Ok(timer.unwrap_or_default()) @@ -172,10 +185,13 @@ impl ChatId { context .sql .execute( - "UPDATE chats + sqlx::query( + "UPDATE chats SET ephemeral_timer=? WHERE id=?;", - paramsv![timer, self], + ) + .bind(timer) + .bind(self), ) .await?; @@ -214,44 +230,45 @@ pub(crate) async fn stock_ephemeral_timer_changed( from_id: u32, ) -> String { match timer { - Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await, + Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await, Timer::Enabled { duration } => match duration { 0..=59 => { - stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await + stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32) + .await } - 60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await, + 60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await, 61..=3599 => { stock_str::msg_ephemeral_timer_minutes( context, format!("{}", (f64::from(duration) / 6.0).round() / 10.0), - from_id, + from_id as u32, ) .await } - 3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await, + 3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await, 3601..=86399 => { stock_str::msg_ephemeral_timer_hours( context, format!("{}", (f64::from(duration) / 360.0).round() / 10.0), - from_id, + from_id as u32, ) .await } - 86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await, + 86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await, 86401..=604_799 => { stock_str::msg_ephemeral_timer_days( context, format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), - from_id, + from_id as u32, ) .await } - 604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await, + 604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await, _ => { stock_str::msg_ephemeral_timer_weeks( context, format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), - from_id, + from_id as u32, ) .await } @@ -261,33 +278,38 @@ pub(crate) async fn stock_ephemeral_timer_changed( impl MsgId { /// Returns ephemeral message timer value for the message. - pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result { + pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result { let res = match context .sql - .query_get_value_result( - "SELECT ephemeral_timer FROM msgs WHERE id=?", - paramsv![self], + .query_get_value::<_, i64>( + sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self), ) .await? { None | Some(0) => Timer::Disabled, - Some(duration) => Timer::Enabled { duration }, + Some(duration) => Timer::Enabled { + duration: u32::try_from(duration)?, + }, }; Ok(res) } /// Starts ephemeral message timer for the message if it is not started yet. - pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> { + pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> { if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? { let ephemeral_timestamp = time() + i64::from(duration); context .sql .execute( - "UPDATE msgs SET ephemeral_timestamp = ? \ + sqlx::query( + "UPDATE msgs SET ephemeral_timestamp = ? \ WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \ AND id = ?", - paramsv![ephemeral_timestamp, ephemeral_timestamp, self], + ) + .bind(ephemeral_timestamp) + .bind(ephemeral_timestamp) + .bind(self), ) .await?; schedule_ephemeral_task(context).await; @@ -308,20 +330,29 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result 0; - if let Some(delete_device_after) = context.get_config_delete_device_after().await { + if let Some(delete_device_after) = context.get_config_delete_device_after().await? { let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) .await .unwrap_or_default() @@ -340,21 +371,22 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result ? \ AND chat_id != ? \ AND chat_id != ?", - paramsv![ - DC_CHAT_ID_TRASH, - threshold_timestamp, - DC_CHAT_ID_LAST_SPECIAL, - self_chat_id, - device_chat_id - ], + ) + .bind(DC_CHAT_ID_TRASH) + .bind(threshold_timestamp) + .bind(DC_CHAT_ID_LAST_SPECIAL) + .bind(self_chat_id) + .bind(device_chat_id), ) - .await?; + .await + .context("deleted update failed")?; updated |= rows_modified > 0; } @@ -376,14 +408,18 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result = match context .sql - .query_get_value_result( - "SELECT ephemeral_timestamp \ - FROM msgs \ - WHERE ephemeral_timestamp != 0 \ - AND chat_id != ? \ - ORDER BY ephemeral_timestamp ASC \ - LIMIT 1", - paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them + .query_get_value( + sqlx::query( + r#" + SELECT ephemeral_timestamp + FROM msgs + WHERE ephemeral_timestamp != 0 + AND chat_id != ? + ORDER BY ephemeral_timestamp ASC + LIMIT 1; + "#, + ) + .bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them ) .await { @@ -439,25 +475,34 @@ pub async fn schedule_ephemeral_task(context: &Context) { pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result> { let now = time(); - let threshold_timestamp = match context.get_config_delete_server_after().await { + let threshold_timestamp = match context.get_config_delete_server_after().await? { None => 0, Some(delete_server_after) => now - delete_server_after, }; - context + let row = context .sql - .query_row_optional( - "SELECT id FROM msgs \ + .fetch_optional( + sqlx::query( + "SELECT id FROM msgs \ WHERE ( \ timestamp < ? \ OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \ ) \ AND server_uid != 0 \ LIMIT 1", - paramsv![threshold_timestamp, now], - |row| row.get::<_, MsgId>(0), + ) + .bind(threshold_timestamp) + .bind(now), ) - .await + .await?; + + if let Some(row) = row { + let msg_id = row.try_get(0)?; + Ok(Some(msg_id)) + } else { + Ok(None) + } } /// Start ephemeral timers for seen messages if they are not started @@ -473,17 +518,17 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> context .sql .execute( - "UPDATE msgs \ + sqlx::query( + "UPDATE msgs \ SET ephemeral_timestamp = ? + ephemeral_timer \ WHERE ephemeral_timer > 0 \ AND ephemeral_timestamp = 0 \ AND state NOT IN (?, ?, ?)", - paramsv![ - time(), - MessageState::InFresh, - MessageState::InNoticed, - MessageState::OutDraft - ], + ) + .bind(time()) + .bind(MessageState::InFresh) + .bind(MessageState::InNoticed) + .bind(MessageState::OutDraft), ) .await?; @@ -717,7 +762,7 @@ mod tests { } async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) { - let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await; + let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap(); // Check that the chat is empty except for possibly info messages: for item in &chat_items { if let ChatItem::Message { msg_id } = item { @@ -733,8 +778,9 @@ mod tests { assert!(msg.text.is_none_or_empty(), msg.text); let rawtxt: Option = t .sql - .query_get_value(t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id]) - .await; + .query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id)) + .await + .unwrap(); assert!(rawtxt.is_none_or_empty(), rawtxt); } } diff --git a/src/error.rs b/src/error.rs index 13a7dd10b..1c471bdce 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,9 @@ //! # Error handling +#[derive(Debug, thiserror::Error)] +#[error("Out of Range")] +pub struct OutOfRangeError; + #[macro_export] macro_rules! ensure_eq { ($left:expr, $right:expr) => ({ diff --git a/src/html.rs b/src/html.rs index 3d084115f..42f03c192 100644 --- a/src/html.rs +++ b/src/html.rs @@ -13,12 +13,12 @@ use std::pin::Pin; use anyhow::Result; use lettre_email::mime::{self, Mime}; -use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::{Message, MsgId}; use crate::mimeparser::parse_message_id; use crate::param::Param::SendHtml; use crate::plaintext::PlainText; +use crate::{context::Context, message}; use lettre_email::PartBuilder; use mailparse::ParsedContentType; @@ -244,32 +244,20 @@ impl MsgId { /// this is the case at least when `Message.has_html()` returns true /// (we do not save raw mime unconditionally in the database to save space). /// The corresponding ffi-function is `dc_get_msg_html()`. - pub async fn get_html(self, context: &Context) -> Option { - let rawmime: Option = context - .sql - .query_get_value( - context, - "SELECT mime_headers FROM msgs WHERE id=?;", - paramsv![self], - ) - .await; + pub async fn get_html(self, context: &Context) -> Result> { + let rawmime = message::get_mime_headers(context, self).await?; - if let Some(rawmime) = rawmime { - if !rawmime.is_empty() { - match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await { - Err(err) => { - warn!(context, "get_html: parser error: {}", err); - None - } - Ok(parser) => Some(parser.html), + if !rawmime.is_empty() { + match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await { + Err(err) => { + warn!(context, "get_html: parser error: {}", err); + Ok(None) } - } else { - warn!(context, "get_html: empty mime for {}", self); - None + Ok(parser) => Ok(Some(parser.html)), } } else { warn!(context, "get_html: no mime for {}", self); - None + Ok(None) } } } @@ -439,7 +427,7 @@ test some special html-characters as < > and & but also " and &#x async fn test_get_html_empty() { let t = TestContext::new().await; let msg_id = MsgId::new_unset(); - assert!(msg_id.get_html(&t).await.is_none()) + assert!(msg_id.get_html(&t).await.unwrap().is_none()) } #[async_std::test] @@ -460,7 +448,7 @@ test some special html-characters as < > and & but also " and &#x assert!(!msg.is_forwarded()); assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap(); + let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); // alice: create chat with bob and forward received html-message there @@ -474,7 +462,7 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap(); + let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); // bob: check that bob also got the html-part of the forwarded message @@ -487,7 +475,7 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&bob).await.unwrap(); + let html = msg.get_id().get_html(&bob).await.unwrap().unwrap(); assert!(html.contains("this is html")); } @@ -517,7 +505,7 @@ test some special html-characters as < > and & but also " and &#x // receive the message on another device let alice = TestContext::new_alice().await; - assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db + assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db alice.recv_msg(&msg).await; let chat = alice.get_self_chat().await; let msg = alice.get_last_msg_in(chat.get_id()).await; @@ -527,7 +515,7 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap(); + let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); } @@ -549,7 +537,7 @@ test some special html-characters as < > and & but also " and &#x assert_eq!(msg.get_text(), Some("plain text".to_string())); assert!(!msg.is_forwarded()); assert!(msg.mime_modified); - let html = msg.get_id().get_html(&alice).await.unwrap(); + let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); assert!(html.contains("html text")); // let bob receive the message @@ -559,7 +547,7 @@ test some special html-characters as < > and & but also " and &#x assert_eq!(msg.get_text(), Some("plain text".to_string())); assert!(!msg.is_forwarded()); assert!(msg.mime_modified); - let html = msg.get_id().get_html(&bob).await.unwrap(); + let html = msg.get_id().get_html(&bob).await.unwrap().unwrap(); assert!(html.contains("html text")); } } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 0395b8864..cc3b0ec37 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -229,7 +229,7 @@ impl Imap { let addr: &str = config.addr.as_ref(); if let Some(token) = - dc_get_oauth2_access_token(context, addr, imap_pw, true).await + dc_get_oauth2_access_token(context, addr, imap_pw, true).await? { let auth = OAuth2 { user: imap_user.into(), @@ -267,7 +267,7 @@ impl Imap { let lock = context.wrong_pw_warning_mutex.lock().await; if self.login_failed_once - && context.get_config_bool(Config::NotifyAboutWrongPw).await + && context.get_config_bool(Config::NotifyAboutWrongPw).await? { if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await { warn!(context, "{}", e); @@ -339,11 +339,11 @@ impl Imap { if self.is_connected() && !self.should_reconnect() { return Ok(()); } - if !context.is_configured().await { + if !context.is_configured().await? { bail!("IMAP Connect without configured params"); } - let param = LoginParam::from_database(context, "configured_").await; + let param = LoginParam::from_database(context, "configured_").await?; // the trailing underscore is correct if let Err(err) = self @@ -521,24 +521,29 @@ impl Imap { // Write collected UIDs to SQLite database. context .sql - .with_conn(move |mut conn| { - let conn2 = &mut conn; - let tx = conn2.transaction()?; - tx.execute( - "UPDATE msgs SET server_uid=0 WHERE server_folder=?", - params![folder], - )?; - for (uid, rfc724_mid) in &msg_ids { - // This may detect previously undetected moved - // messages, so we update server_folder too. - tx.execute( - "UPDATE msgs \ - SET server_folder=?,server_uid=? WHERE rfc724_mid=?", - params![folder, uid, rfc724_mid], - )?; - } - tx.commit()?; - Ok(()) + .transaction(|conn| { + Box::pin(async move { + sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?") + .bind(&folder) + .execute(&mut *conn) + .await?; + + for (uid, rfc724_mid) in &msg_ids { + // This may detect previously undetected moved + // messages, so we update server_folder too. + sqlx::query( + "UPDATE msgs \ + SET server_folder=?,server_uid=? WHERE rfc724_mid=?", + ) + .bind(&folder) + .bind(uid) + .bind(rfc724_mid) + .execute(&mut *conn) + .await?; + } + + Ok(()) + }) }) .await?; Ok(()) @@ -655,7 +660,7 @@ impl Imap { folder: S, fetch_existing_msgs: bool, ) -> Result { - let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await) + let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) .unwrap_or_default(); let new_emails = self @@ -754,7 +759,7 @@ impl Imap { let session = self.session.as_mut().unwrap(); let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .ok_or_else(|| format_err!("Not configured"))?; let search_command = format!("FROM \"{}\"", self_addr); @@ -1276,10 +1281,7 @@ impl Imap { context: &Context, create_mvbox: bool, ) -> Result<()> { - let folders_configured = context - .sql - .get_raw_config_int(context, "folders_configured") - .await; + let folders_configured = context.sql.get_raw_config_int("folders_configured").await?; if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION { return Ok(()); } @@ -1407,7 +1409,7 @@ impl Imap { } context .sql - .set_raw_config_int(context, "folders_configured", DC_FOLDERS_CONFIGURED_VERSION) + .set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION) .await?; } info!(context, "FINISHED configuring IMAP-folders."); @@ -1519,7 +1521,7 @@ async fn precheck_imf( "[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid ); - let delete_server_after = context.get_config_delete_server_after().await; + let delete_server_after = context.get_config_delete_server_after().await?; if delete_server_after != Some(0) { if msg_id @@ -1729,9 +1731,15 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) context .sql .execute( - "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) + sqlx::query( + "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;", - paramsv![folder, 0u32, uid_next, uid_next, folder], + ) + .bind(folder) + .bind(0i32) + .bind(uid_next as i64) + .bind(uid_next as i64) + .bind(folder), ) .await?; Ok(()) @@ -1745,10 +1753,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) async fn get_uid_next(context: &Context, folder: &str) -> Result { Ok(context .sql - .query_get_value_result( - "SELECT uid_next FROM imap_sync WHERE folder=?;", - paramsv![folder], - ) + .query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder)) .await? .unwrap_or(0)) } @@ -1761,9 +1766,15 @@ pub(crate) async fn set_uidvalidity( context .sql .execute( - "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) + sqlx::query( + "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;", - paramsv![folder, uidvalidity, 0u32, uidvalidity, folder], + ) + .bind(folder) + .bind(uidvalidity as i32) + .bind(0i32) + .bind(uidvalidity as i32) + .bind(folder), ) .await?; Ok(()) @@ -1772,26 +1783,28 @@ pub(crate) async fn set_uidvalidity( async fn get_uidvalidity(context: &Context, folder: &str) -> Result { Ok(context .sql - .query_get_value_result( - "SELECT uidvalidity FROM imap_sync WHERE folder=?;", - paramsv![folder], + .query_get_value( + sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder), ) .await? .unwrap_or(0)) } /// Deprecated, use get_uid_next() and get_uidvalidity() -pub async fn get_config_last_seen_uid>(context: &Context, folder: S) -> (u32, u32) { +pub async fn get_config_last_seen_uid>( + context: &Context, + folder: S, +) -> Result<(u32, u32)> { let key = format!("imap.mailbox.{}", folder.as_ref()); - if let Some(entry) = context.sql.get_raw_config(context, &key).await { + if let Some(entry) = context.sql.get_raw_config(&key).await? { // the entry has the format `imap.mailbox.=:` let mut parts = entry.split(':'); - ( + Ok(( parts.next().unwrap_or_default().parse().unwrap_or(0), parts.next().unwrap_or_default().parse().unwrap_or(0), - ) + )) } else { - (0, 0) + Ok((0, 0)) } } diff --git a/src/imap/scan_folders.rs b/src/imap/scan_folders.rs index 248970091..304a4fdd8 100644 --- a/src/imap/scan_folders.rs +++ b/src/imap/scan_folders.rs @@ -17,7 +17,7 @@ impl Imap { let elapsed_secs = last_scan.elapsed().as_secs(); let debounce_secs = context .get_config_u64(Config::ScanAllFoldersDebounceSecs) - .await; + .await?; if elapsed_secs < debounce_secs { return Ok(()); @@ -95,8 +95,8 @@ async fn get_watched_folders(context: &Context) -> Vec { (Config::InboxWatch, Config::ConfiguredInboxFolder), ]; for (watched, configured) in folder_watched_configured { - if context.get_config_bool(*watched).await { - if let Some(folder) = context.get_config(*configured).await { + if context.get_config_bool(*watched).await.unwrap_or_default() { + if let Ok(Some(folder)) = context.get_config(*configured).await { res.push(folder); } } diff --git a/src/imex.rs b/src/imex.rs index ad97b5c01..dd448e593 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -10,6 +10,7 @@ use async_std::{ prelude::*, }; use rand::{thread_rng, Rng}; +use sqlx::Row; use crate::chat; use crate::chat::delete_and_reset_all_device_msgs; @@ -38,7 +39,7 @@ const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite"; const BLOBS_BACKUP_NAME: &str = "blobs_backup"; #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] -#[repr(i32)] +#[repr(u32)] pub enum ImexMode { /// Export all private keys and all public keys of the user to the /// directory given as `param1`. The default key is written to the files `public-key-default.asc` @@ -170,8 +171,8 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef) -> Re match sql.open(context, &path, true).await { Ok(_) => { let curr_backup_time = sql - .get_raw_config_int(context, "backup_time") - .await + .get_raw_config_int("backup_time") + .await? .unwrap_or_default(); if curr_backup_time > newest_backup_time { newest_backup_path = Some(path); @@ -271,7 +272,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result None, true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), }; @@ -333,7 +334,7 @@ pub fn create_setup_code(_context: &Context) -> String { } async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { - if !context.sql.get_raw_config_bool(context, "bcc_self").await { + if !context.sql.get_raw_config_bool("bcc_self").await? { let mut msg = Message::new(Viewtype::Text); // TODO: define this as a stockstring once the wording is settled. msg.text = Some( @@ -394,7 +395,7 @@ async fn set_self_key( }; context .sql - .set_raw_config_int(context, "e2ee_enabled", e2ee_enabled) + .set_raw_config_int("e2ee_enabled", e2ee_enabled) .await?; } None => { @@ -404,7 +405,7 @@ async fn set_self_key( } }; - let self_addr = context.get_config(Config::ConfiguredAddr).await; + let self_addr = context.get_config(Config::ConfiguredAddr).await?; ensure!(self_addr.is_some(), "Missing self addr"); let addr = EmailAddress::new(&self_addr.unwrap_or_default())?; let keypair = pgp::KeyPair { @@ -493,7 +494,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef) -> ); ensure!( - !context.is_configured().await, + !context.is_configured().await?, "Cannot import backups to accounts in use." ); ensure!( @@ -564,7 +565,7 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef ); ensure!( - !context.is_configured().await, + !context.is_configured().await?, "Cannot import backups to accounts in use." ); ensure!( @@ -594,9 +595,9 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef let total_files_cnt = context .sql - .query_get_value::(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![]) - .await - .unwrap_or_default() as usize; + .count("SELECT COUNT(*) FROM backup_blobs;") + .await?; + info!( context, "***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt, @@ -606,29 +607,25 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef // consuming too much memory. let file_ids = context .sql - .query_map( - "SELECT id FROM backup_blobs ORDER BY id", - paramsv![], - |row| row.get(0), - |ids| { - ids.collect::, _>>() - .map_err(Into::into) - }, - ) + .fetch("SELECT id FROM backup_blobs ORDER BY id") + .await? + .map(|row| row?.try_get(0)) + .collect::>>() .await?; let mut all_files_extracted = true; for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() { // Load a single blob into memory - let (file_name, file_blob) = context + let row = context .sql - .query_row( - "SELECT file_name, file_content FROM backup_blobs WHERE id = ?", - paramsv![file_id], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec>(1)?)), + .fetch_one( + sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?") + .bind(file_id), ) .await?; + let file_name: String = row.try_get(0)?; + let file_blob: &[u8] = row.try_get(1)?; if context.shall_stop_ongoing().await { all_files_extracted = false; break; @@ -646,16 +643,13 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef } let path_filename = context.get_blobdir().join(file_name); - dc_write_file(context, &path_filename, &file_blob).await?; + dc_write_file(context, &path_filename, file_blob).await?; } if all_files_extracted { // only delete backup_blobs if all files were successfully extracted - context - .sql - .execute("DROP TABLE backup_blobs;", paramsv![]) - .await?; - context.sql.execute("VACUUM;", paramsv![]).await.ok(); + context.sql.execute("DROP TABLE backup_blobs;").await?; + context.sql.execute("VACUUM;").await.ok(); Ok(()) } else { bail!("received stop signal"); @@ -674,13 +668,13 @@ async fn export_backup(context: &Context, dir: impl AsRef) -> Result<()> { context .sql - .set_raw_config_int(context, "backup_time", now as i32) + .set_raw_config_int("backup_time", now as i32) .await?; sql::housekeeping(context).await.ok_or_log(context); context .sql - .execute("VACUUM;", paramsv![]) + .execute("VACUUM;") .await .map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e)); @@ -833,29 +827,24 @@ async fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<() async fn export_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { let mut export_errors = 0; - let keys = context + let mut 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 = SignedPublicKey::from_slice(&public_key_blob); - let private_key_blob: Vec = row.get(2)?; - let private_key = SignedSecretKey::from_slice(&private_key_blob); - let is_default: i32 = row.get(3)?; + .fetch("SELECT id, public_key, private_key, is_default FROM keypairs;") + .await? + .map(|row| -> sqlx::Result<_> { + let row = row?; + let id = row.try_get(0)?; + let public_key_blob: &[u8] = row.try_get(1)?; + let public_key = SignedPublicKey::from_slice(public_key_blob); + let private_key_blob: &[u8] = row.try_get(2)?; + let private_key = SignedSecretKey::from_slice(private_key_blob); + let is_default: i32 = row.try_get(3)?; - Ok((id, public_key, private_key, is_default)) - }, - |keys| { - keys.collect::, _>>() - .map_err(Into::into) - }, - ) - .await?; + Ok((id, public_key, private_key, is_default)) + }); - for (id, public_key, private_key, is_default) in keys { + while let Some(parts) = keys.next().await { + let (id, public_key, private_key, is_default) = parts?; let id = Some(id).filter(|_| is_default != 0); if let Ok(key) = public_key { if export_key_to_asc_file(context, &dir, id, &key) diff --git a/src/job.rs b/src/job.rs index 978f5eeca..6aeb6d9c8 100644 --- a/src/job.rs +++ b/src/job.rs @@ -7,10 +7,11 @@ use std::{fmt, time::Duration}; use anyhow::{bail, ensure, format_err, Context as _, Error, Result}; use async_smtp::smtp::response::{Category, Code, Detail}; +use async_std::prelude::*; use async_std::task::sleep; -use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use rand::{thread_rng, Rng}; +use sqlx::Row; use crate::dc_tools::{dc_delete_file, dc_read_file, time}; use crate::ephemeral::load_imap_deletion_msgid; @@ -36,10 +37,8 @@ use crate::{scheduler::InterruptInfo, sql}; const JOB_RETRIES: u32 = 17; /// Thread IDs -#[derive( - Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] -#[repr(i32)] +#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)] +#[repr(u32)] pub(crate) enum Thread { Unknown = 0, Imap = 100, @@ -76,19 +75,9 @@ impl Default for Thread { } #[derive( - Debug, - Display, - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - FromPrimitive, - ToPrimitive, - FromSql, - ToSql, + Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type, )] -#[repr(i32)] +#[repr(u32)] pub enum Action { Unknown = 0, @@ -184,7 +173,7 @@ impl Job { if self.job_id != 0 { context .sql - .execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32]) + .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32)) .await?; } @@ -203,26 +192,24 @@ impl Job { context .sql .execute( - "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", - paramsv![ - self.desired_timestamp, - self.tries as i64, - self.param.to_string(), - self.job_id as i32, - ], + sqlx::query( + "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", + ) + .bind(self.desired_timestamp) + .bind(self.tries as i64) + .bind(self.param.to_string()) + .bind(self.job_id as i32), ) .await?; } else { context.sql.execute( - "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);", - paramsv![ - self.added_timestamp, - thread, - self.action, - self.foreign_id, - self.param.to_string(), - self.desired_timestamp - ] + sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);") + .bind(self.added_timestamp) + .bind(thread) + .bind(self.action) + .bind(self.foreign_id) + .bind(self.param.to_string()) + .bind(self.desired_timestamp) ).await?; } @@ -253,7 +240,7 @@ impl Job { let status = match smtp.send(context, recipients, message, job_id).await { Err(crate::smtp::send::Error::SendError(err)) => { // Remote error, retry later. - warn!(context, "SMTP failed to send: {}", err); + warn!(context, "SMTP failed to send: {:?}", err); self.pending_error = Some(err.to_string()); let res = match err { @@ -339,6 +326,12 @@ impl Job { error!(context, "SMTP job failed because SMTP has no transport"); Status::Finished(Err(format_err!("SMTP has not transport"))) } + Err(crate::smtp::send::Error::Other(err)) => { + // Local error, job is invalid, do not retry. + smtp.disconnect().await; + warn!(context, "unable to load job: {}", err); + Status::Finished(Err(err)) + } Ok(()) => { job_try!(success_cb().await); Status::Finished(Ok(())) @@ -387,11 +380,21 @@ impl Job { /* if there is a msg-id and it does not exist in the db, cancel sending. this happends if dc_delete_msgs() was called before the generated mime was sent out */ - if 0 != self.foreign_id && !message::exists(context, MsgId::new(self.foreign_id)).await { - return Status::Finished(Err(format_err!( - "Not sending Message {} as it was deleted", - self.foreign_id - ))); + if 0 != self.foreign_id { + match message::exists(context, MsgId::new(self.foreign_id)).await { + Ok(exists) => { + if !exists { + return Status::Finished(Err(format_err!( + "Not sending Message {} as it was deleted", + self.foreign_id + ))); + } + } + Err(err) => { + warn!(context, "failed to check message existence: {:?}", err); + return Status::RetryLater; + } + } }; let foreign_id = self.foreign_id; @@ -399,7 +402,7 @@ impl Job { async move { // smtp success, update db ASAP, then delete smtp file if 0 != foreign_id { - set_delivered(context, MsgId::new(foreign_id)).await; + set_delivered(context, MsgId::new(foreign_id)).await?; } // now also delete the generated file dc_delete_file(context, filename).await; @@ -416,44 +419,38 @@ impl Job { contact_id: u32, ) -> sql::Result<(Vec, Vec)> { // Extract message IDs from job parameters - let res: Vec<(u32, MsgId)> = context + let mut rows = context .sql - .query_map( - "SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?", - paramsv![contact_id, self.job_id], - |row| { - let job_id: u32 = row.get(0)?; - let params_str: String = row.get(1)?; - let params: Params = params_str.parse().unwrap_or_default(); - Ok((job_id, params)) - }, - |jobs| { - let res = jobs - .filter_map(|row| { - let (job_id, params) = row.ok()?; - let msg_id = params.get_msg_id()?; - Some((job_id, msg_id)) - }) - .collect(); - Ok(res) - }, + .fetch( + sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?") + .bind(contact_id) + .bind(self.job_id), ) .await?; // Load corresponding RFC724 message IDs let mut job_ids = Vec::new(); let mut rfc724_mids = Vec::new(); - for (job_id, msg_id) in res { - if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await { - job_ids.push(job_id); - rfc724_mids.push(rfc724_mid); + + while let Some(row) = rows.next().await { + let row = row?; + let job_id: u32 = row.try_get(0)?; + let params_str: String = row.try_get(1)?; + let params: Params = params_str.parse().unwrap_or_default(); + if let Some(msg_id) = params.get_msg_id() { + if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await + { + job_ids.push(job_id); + rfc724_mids.push(rfc724_mid); + } } } Ok((job_ids, rfc724_mids)) } async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status { - if !context.get_config_bool(Config::MdnsEnabled).await { + let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await); + if !mdns_enabled { // User has disabled MDNs after job scheduling but before // execution. return Status::Finished(Err(format_err!("MDNs are disabled"))); @@ -539,7 +536,13 @@ impl Job { ); return Status::Finished(Ok(())); } - Ok(Some(config)) => context.get_config(config).await, + Ok(Some(config)) => match context.get_config(config).await { + Ok(folder) => folder, + Err(err) => { + warn!(context, "failed to load config: {}", err); + return Status::RetryLater; + } + }, }; if let Some(dest_folder) = dest_folder { @@ -657,7 +660,7 @@ impl Job { /// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server /// and show them in the chat list. async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status { - if context.get_config_bool(Config::Bot).await { + if job_try!(context.get_config_bool(Config::Bot).await) { return Status::Finished(Ok(())); // Bots don't want those messages } if let Err(err) = imap.connect_configured(context).await { @@ -669,13 +672,13 @@ impl Job { add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await; add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await; - if context.get_config_bool(Config::FetchExistingMsgs).await { + if job_try!(context.get_config_bool(Config::FetchExistingMsgs).await) { for config in &[ Config::ConfiguredMvboxFolder, Config::ConfiguredInboxFolder, Config::ConfiguredSentboxFolder, ] { - if let Some(folder) = context.get_config(*config).await { + if let Some(folder) = job_try!(context.get_config(*config).await) { if let Err(e) = imap.fetch_new_messages(context, folder, true).await { // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: warn!(context, "Could not fetch messages, retrying: {:#}", e); @@ -688,7 +691,7 @@ impl Job { // Make sure that if there now is a chat with a contact (created by an outgoing // message), then group contact requests from this contact should also be unblocked. // See https://github.com/deltachat/deltachat-core-rust/issues/2097. - for item in chat::get_chat_msgs(context, ChatId::new(DC_CHAT_ID_DEADDROP), 0, None).await { + for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) { if let ChatItem::Message { msg_id } = item { let msg = match Message::load_from_db(context, msg_id).await { Err(e) => { @@ -736,26 +739,21 @@ impl Job { return Status::RetryLater; } - if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await { - job_try!( - imap.resync_folder_uids(context, sentbox_folder.to_string()) - .await - ); + let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await); + if let Some(sentbox_folder) = sentbox_folder { + job_try!(imap.resync_folder_uids(context, sentbox_folder).await); } - if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await { - job_try!( - imap.resync_folder_uids(context, inbox_folder.to_string()) - .await - ); + let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await); + if let Some(inbox_folder) = inbox_folder { + job_try!(imap.resync_folder_uids(context, inbox_folder).await); } - if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await { - job_try!( - imap.resync_folder_uids(context, mvbox_folder.to_string()) - .await - ); + let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await); + if let Some(mvbox_folder) = mvbox_folder { + job_try!(imap.resync_folder_uids(context, mvbox_folder).await); } + Status::Finished(Ok(())) } @@ -803,11 +801,13 @@ impl Job { // the name sent in the From field by the user. if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() - && context.get_config_bool(Config::MdnsEnabled).await { - if let Err(err) = send_mdn(context, &msg).await { - warn!(context, "could not send out mdn for {}: {}", msg.id, err); - return Status::Finished(Err(err)); + let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await); + if mdns_enabled { + if let Err(err) = send_mdn(context, &msg).await { + warn!(context, "could not send out mdn for {}: {}", msg.id, err); + return Status::Finished(Err(err)); + } } } Status::Finished(Ok(())) @@ -820,50 +820,46 @@ impl Job { pub async fn kill_action(context: &Context, action: Action) -> bool { context .sql - .execute("DELETE FROM jobs WHERE action=?;", paramsv![action]) + .execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action)) .await .is_ok() } /// Remove jobs with specified IDs. async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> { - context - .sql - .execute( - format!( - "DELETE FROM jobs WHERE id IN({})", - job_ids.iter().map(|_| "?").join(",") - ), - job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(), - ) - .await?; + let q = format!( + "DELETE FROM jobs WHERE id IN({})", + job_ids.iter().map(|_| "?").join(",") + ); + let mut query = sqlx::query(&q); + for id in job_ids { + query = query.bind(*id); + } + context.sql.execute(query).await?; Ok(()) } pub async fn action_exists(context: &Context, action: Action) -> bool { context .sql - .exists("SELECT id FROM jobs WHERE action=?;", paramsv![action]) + .exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action)) .await .unwrap_or_default() } -async fn set_delivered(context: &Context, msg_id: MsgId) { +async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> { message::update_msg_state(context, msg_id, MessageState::OutDelivered).await; let chat_id: ChatId = context .sql - .query_get_value( - context, - "SELECT chat_id FROM msgs WHERE id=?", - paramsv![msg_id], - ) - .await + .query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id)) + .await? .unwrap_or_default(); context.emit_event(EventType::MsgDelivered { chat_id, msg_id }); + Ok(()) } async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) { - let mailbox = if let Some(m) = context.get_config(folder).await { + let mailbox = if let Ok(Some(m)) = context.get_config(folder).await { m } else { return; @@ -933,14 +929,14 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result Result Result Result { +pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result { ensure!( action != Action::Unknown, "Invalid action passed to job_add" ); - Ok(Job::new(action, foreign_id as u32, param, delay_seconds)) + Ok(Job::new(action, foreign_id, param, delay_seconds)) } /// Adds a job to the database, scheduling it. @@ -1245,7 +1241,13 @@ pub async fn add(context: &Context, job: Job) { } async fn load_housekeeping_job(context: &Context) -> Option { - let last_time = context.get_config_i64(Config::LastHousekeeping).await; + let last_time = match context.get_config_i64(Config::LastHousekeeping).await { + Ok(last_time) => last_time, + Err(err) => { + warn!(context, "failed to load housekeeping config: {:?}", err); + return None; + } + }; let next_time = last_time + (60 * 60 * 24); if next_time <= time() { @@ -1280,65 +1282,77 @@ pub(crate) async fn load_next( sleep(Duration::from_millis(500)).await; } - let query; - let params; let t = time(); - let m; let thread_i = thread as i64; - if let Some(msg_id) = info.msg_id { - query = r#" + let get_query = || { + if let Some(msg_id) = info.msg_id { + sqlx::query( + r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND foreign_id=? ORDER BY action DESC, added_timestamp LIMIT 1; -"#; - m = msg_id; - params = paramsv![thread_i, m]; - } else if !info.probe_network { - // processing for first-try and after backoff-timeouts: - // process jobs in the order they were added. - query = r#" +"#, + ) + .bind(thread_i) + .bind(msg_id) + } else if !info.probe_network { + // processing for first-try and after backoff-timeouts: + // process jobs in the order they were added. + sqlx::query( + r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND desired_timestamp<=? ORDER BY action DESC, added_timestamp LIMIT 1; -"#; - params = paramsv![thread_i, t]; - } else { - // processing after call to dc_maybe_network(): - // process _all_ pending jobs that failed before - // in the order of their backoff-times. - query = r#" +"#, + ) + .bind(thread_i) + .bind(t) + } else { + // processing after call to dc_maybe_network(): + // process _all_ pending jobs that failed before + // in the order of their backoff-times. + sqlx::query( + r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND tries>0 ORDER BY desired_timestamp, action DESC LIMIT 1; -"#; - params = paramsv![thread_i]; +"#, + ) + .bind(thread_i) + } }; let job = loop { let job_res = context .sql - .query_row_optional(query, params.clone(), |row| { - let job = Job { - job_id: row.get("id")?, - action: row.get("action")?, - foreign_id: row.get("foreign_id")?, - desired_timestamp: row.get("desired_timestamp")?, - added_timestamp: row.get("added_timestamp")?, - tries: row.get("tries")?, - param: row.get::<_, String>("param")?.parse().unwrap_or_default(), - pending_error: None, - }; - - Ok(job) - }) - .await; + .fetch_optional(get_query()) + .await + .and_then(|row| { + if let Some(row) = row { + Ok(Some(Job { + job_id: row.try_get("id")?, + action: row.try_get("action")?, + foreign_id: row.try_get("foreign_id")?, + desired_timestamp: row.try_get("desired_timestamp")?, + added_timestamp: row.try_get("added_timestamp")?, + tries: row.try_get::("tries")? as u32, + param: row + .try_get::("param")? + .parse() + .unwrap_or_default(), + pending_error: None, + })) + } else { + Ok(None) + } + }); match job_res { Ok(job) => break job, @@ -1349,15 +1363,18 @@ LIMIT 1; // TODO: improve by only doing a single query match context .sql - .query_row(query, params.clone(), |row| row.get::<_, i32>(0)) + .fetch_one(get_query()) .await + .and_then(|row| row.try_get::(0).map_err(Into::into)) { Ok(id) => { - context + if let Err(err) = context .sql - .execute("DELETE FROM jobs WHERE id=?;", paramsv![id]) + .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id)) .await - .ok(); + { + warn!(context, "failed to delete job {}: {:?}", id, err); + } } Err(err) => { error!(context, "failed to retrieve invalid job from DB: {}", err); @@ -1399,22 +1416,22 @@ mod tests { use crate::test_utils::TestContext; - async fn insert_job(context: &Context, foreign_id: i64) { + async fn insert_job(context: &Context, foreign_id: i64, valid: bool) { let now = time(); context .sql .execute( - "INSERT INTO jobs + sqlx::query( + "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?, ?, ?, ?, ?, ?);", - paramsv![ - now, - Thread::from(Action::MoveMsg), - Action::MoveMsg, - foreign_id, - Params::new().to_string(), - now - ], + ) + .bind(now) + .bind(Thread::from(Action::MoveMsg)) + .bind(if valid { Action::MoveMsg as i32 } else { -1 }) + .bind(foreign_id) + .bind(Params::new().to_string()) + .bind(now), ) .await .unwrap(); @@ -1426,7 +1443,7 @@ mod tests { // fails to load from the database instead of failing to load // all jobs. let t = TestContext::new().await; - insert_job(&t, -1).await; // This can not be loaded into Job struct. + insert_job(&t, 1, false).await; // This can not be loaded into Job struct. let jobs = load_next( &t, Thread::from(Action::MoveMsg), @@ -1436,7 +1453,7 @@ mod tests { // The housekeeping job should be loaded as we didn't run housekeeping in the last day: assert!(jobs.unwrap().action == Action::Housekeeping); - insert_job(&t, 1).await; + insert_job(&t, 1, true).await; let jobs = load_next( &t, Thread::from(Action::MoveMsg), @@ -1450,7 +1467,7 @@ mod tests { async fn test_load_next_job_one() { let t = TestContext::new().await; - insert_job(&t, 1).await; + insert_job(&t, 1, true).await; let jobs = load_next( &t, diff --git a/src/key.rs b/src/key.rs index 30aa8ce9d..82554f27c 100644 --- a/src/key.rs +++ b/src/key.rs @@ -9,6 +9,7 @@ use num_traits::FromPrimitive; use pgp::composed::Deserializable; use pgp::ser::Serialize; use pgp::types::{KeyTrait, SecretKeyTrait}; +use sqlx::Row; use thiserror::Error; use crate::config::Config; @@ -41,6 +42,10 @@ pub enum Error { InvalidConfiguredAddr(#[from] InvalidEmailError), #[error("no data provided")] Empty, + #[error("db: {}", _0)] + Sql(#[from] sqlx::Error), + #[error("{0}")] + Other(#[from] anyhow::Error), } pub type Result = std::result::Result; @@ -118,24 +123,21 @@ impl DcKey for SignedPublicKey { async fn load_self(context: &Context) -> Result { match context .sql - .query_row( + .fetch_optional( r#" SELECT public_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - paramsv![], - |row| row.get::<_, Vec>(0), ) - .await + .await? { - Ok(bytes) => Self::from_slice(&bytes), - Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { + Some(row) => Self::from_slice(row.try_get(0)?), + None => { let keypair = generate_keypair(context).await?; Ok(keypair.public) } - Err(err) => Err(err.into()), } } @@ -163,24 +165,21 @@ impl DcKey for SignedSecretKey { async fn load_self(context: &Context) -> Result { match context .sql - .query_row( + .fetch_optional( r#" SELECT private_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - paramsv![], - |row| row.get::<_, Vec>(0), ) - .await + .await? { - Ok(bytes) => Self::from_slice(&bytes), - Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { + Some(row) => Self::from_slice(row.try_get(0)?), + None => { let keypair = generate_keypair(context).await?; Ok(keypair.secret) } - Err(err) => Err(err.into()), } } @@ -221,7 +220,7 @@ impl DcSecretKey for SignedSecretKey { async fn generate_keypair(context: &Context) -> Result { let addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .ok_or(Error::NoConfiguredAddr)?; let addr = EmailAddress::new(&addr)?; let _guard = context.generating_key_mutex.lock().await; @@ -229,26 +228,27 @@ async fn generate_keypair(context: &Context) -> Result { // Check if the key appeared while we were waiting on the lock. match context .sql - .query_row( - r#" + .fetch_optional( + sqlx::query( + r#" SELECT public_key, private_key FROM keypairs WHERE addr=?1 AND is_default=1; "#, - paramsv![addr], - |row| Ok((row.get::<_, Vec>(0)?, row.get::<_, Vec>(1)?)), + ) + .bind(addr.to_string()), ) - .await + .await? { - Ok((pub_bytes, sec_bytes)) => Ok(KeyPair { + Some(row) => Ok(KeyPair { addr, - public: SignedPublicKey::from_slice(&pub_bytes)?, - secret: SignedSecretKey::from_slice(&sec_bytes)?, + public: SignedPublicKey::from_slice(row.try_get(0)?)?, + secret: SignedSecretKey::from_slice(row.try_get(1)?)?, }), - Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { + None => { let start = std::time::SystemTime::now(); - let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await) + let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?) .unwrap_or_default(); info!(context, "Generating keypair with type {}", keytype); let keypair = @@ -262,7 +262,6 @@ async fn generate_keypair(context: &Context) -> Result { ); Ok(keypair) } - Err(err) => Err(err.into()), } } @@ -320,15 +319,16 @@ pub async fn store_self_keypair( context .sql .execute( - "DELETE FROM keypairs WHERE public_key=? OR private_key=?;", - paramsv![public_key, secret_key], + sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;") + .bind(&public_key) + .bind(&secret_key), ) .await .map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?; if default == KeyPairUse::Default { context .sql - .execute("UPDATE keypairs SET is_default=0;", paramsv![]) + .execute("UPDATE keypairs SET is_default=0;") .await .map_err(|err| SaveKeyError::new("failed to clear default", err))?; } @@ -340,13 +340,18 @@ pub async fn store_self_keypair( let addr = keypair.addr.to_string(); let t = time(); - let params = paramsv![addr, is_default, public_key, secret_key, t]; context .sql .execute( - "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) + sqlx::query( + "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);", - params, + ) + .bind(addr) + .bind(is_default) + .bind(&public_key) + .bind(&secret_key) + .bind(t), ) .await .map_err(|err| SaveKeyError::new("failed to insert keypair", err))?; @@ -620,7 +625,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD let nrows = || async { ctx.sql - .query_get_value::(&ctx, "SELECT COUNT(*) FROM keypairs;", paramsv![]) + .count("SELECT COUNT(*) FROM keypairs;") .await .unwrap() }; diff --git a/src/lib.rs b/src/lib.rs index eb40102b1..244390c33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ -#![forbid(unsafe_code)] #![deny( clippy::correctness, missing_debug_implementations, clippy::all, clippy::indexing_slicing, clippy::wildcard_imports, - clippy::needless_borrow + clippy::needless_borrow, + unsafe_code )] #![allow(clippy::match_bool, clippy::eval_order_dependence)] @@ -13,16 +13,10 @@ extern crate num_derive; #[macro_use] extern crate smallvec; -#[macro_use] -extern crate rusqlite; extern crate strum; #[macro_use] extern crate strum_macros; -pub trait ToSql: rusqlite::ToSql + Send + Sync {} - -impl ToSql for T {} - #[macro_use] pub mod log; #[macro_use] diff --git a/src/location.rs b/src/location.rs index 576634a72..f3c3230a5 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,8 +1,11 @@ //! Location handling +use std::convert::TryFrom; use anyhow::{ensure, Error}; +use async_std::prelude::*; use bitflags::bitflags; use quick_xml::events::{BytesEnd, BytesStart, BytesText}; +use sqlx::Row; use crate::chat::{self, ChatId}; use crate::config::Config; @@ -198,15 +201,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: if context .sql .execute( - "UPDATE chats \ + sqlx::query( + "UPDATE chats \ SET locations_send_begin=?, \ locations_send_until=? \ WHERE id=?", - paramsv![ - if 0 != seconds { now } else { 0 }, - if 0 != seconds { now + seconds } else { 0 }, - chat_id, - ], + ) + .bind(if 0 != seconds { now } else { 0 }) + .bind(if 0 != seconds { now + seconds } else { 0 }) + .bind(chat_id), ) .await .is_ok() @@ -259,16 +262,17 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option context .sql .exists( - "SELECT id FROM chats WHERE id=? AND locations_send_until>?;", - paramsv![chat_id, time()], + sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;") + .bind(chat_id) + .bind(time()), ) .await .unwrap_or_default(), None => context .sql .exists( - "SELECT id FROM chats WHERE locations_send_until>?;", - paramsv![time()], + sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;") + .bind(time()), ) .await .unwrap_or_default(), @@ -281,28 +285,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 } let mut continue_streaming = false; - if let Ok(chats) = context + if let Ok(mut chats) = context .sql - .query_map( - "SELECT id FROM chats WHERE locations_send_until>?;", - paramsv![time()], - |row| row.get::<_, i32>(0), - |chats| chats.collect::, _>>().map_err(Into::into), - ) + .fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time())) .await + .map(|rows| rows.map(|row| row?.try_get::(0))) { - for chat_id in chats { + while let Some(chat_id) = chats.next().await { + let chat_id = match chat_id { + Ok(id) => id, + Err(_) => break, + }; if let Err(err) = context.sql.execute( + sqlx::query( "INSERT INTO locations \ - (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", - paramsv![ - latitude, - longitude, - accuracy, - time(), - chat_id, - DC_CONTACT_ID_SELF, - ] + (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);" + ) + .bind(latitude) + .bind(longitude) + .bind(accuracy) + .bind(time()) + .bind(chat_id) + .bind(DC_CONTACT_ID_SELF) + ).await { warn!(context, "failed to store location {:?}", err); } else { @@ -324,10 +329,11 @@ pub async fn get_range( contact_id: Option, timestamp_from: i64, mut timestamp_to: i64, -) -> Vec { +) -> Result, Error> { if timestamp_to == 0 { timestamp_to = time() + 10; } + let (disable_chat_id, chat_id) = match chat_id { Some(chat_id) => (0, chat_id), None => (1, ChatId::new(0)), // this ChatId is unused @@ -336,56 +342,52 @@ pub async fn get_range( Some(contact_id) => (0, contact_id), None => (1, 0), // this contact_id is unused }; - context + + let list = context .sql - .query_map( - "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ + .fetch( + sqlx::query( + "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \ FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \ AND (? OR l.from_id=?) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", - paramsv![ - disable_chat_id, - chat_id, - disable_contact_id, - contact_id as i32, - timestamp_from, - timestamp_to, - ], - |row| { - let msg_id = row.get(6)?; - let txt: String = row.get(9)?; - let marker = if msg_id != 0 && is_marker(&txt) { - Some(txt) - } else { - None - }; - let loc = Location { - location_id: row.get(0)?, - latitude: row.get(1)?, - longitude: row.get(2)?, - accuracy: row.get(3)?, - timestamp: row.get(4)?, - independent: row.get(5)?, - msg_id, - contact_id: row.get(7)?, - chat_id: row.get(8)?, - marker, - }; - Ok(loc) - }, - |locations| { - let mut ret = Vec::new(); - - for location in locations { - ret.push(location?); - } - Ok(ret) - }, + ) + .bind(disable_chat_id) + .bind(chat_id) + .bind(disable_contact_id) + .bind(contact_id as i64) + .bind(timestamp_from) + .bind(timestamp_to), ) - .await - .unwrap_or_default() + .await? + .map(|row| { + let row = row?; + let msg_id = row.try_get(6)?; + let txt: String = row.try_get(9)?; + let marker = if msg_id != 0 && is_marker(&txt) { + Some(txt) + } else { + None + }; + let loc = Location { + location_id: row.try_get(0)?, + latitude: row.try_get(1)?, + longitude: row.try_get(2)?, + accuracy: row.try_get(3)?, + timestamp: row.try_get(4)?, + independent: row.try_get(5)?, + msg_id, + contact_id: row.try_get(7)?, + chat_id: row.try_get(8)?, + marker, + }; + Ok(loc) + }) + .collect::>() + .await?; + Ok(list) } fn is_marker(txt: &str) -> bool { @@ -399,10 +401,7 @@ fn is_marker(txt: &str) -> bool { /// Deletes all locations from the database. pub async fn delete_all(context: &Context) -> Result<(), Error> { - context - .sql - .execute("DELETE FROM locations;", paramsv![]) - .await?; + context.sql.execute("DELETE FROM locations;").await?; context.emit_event(EventType::LocationChanged(None)); Ok(()) } @@ -412,19 +411,23 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32) let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); - let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( - "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", - paramsv![chat_id], |row| { - let send_begin: i64 = row.get(0)?; - let send_until: i64 = row.get(1)?; - let last_sent: i64 = row.get(2)?; + let (locations_send_begin, locations_send_until, locations_last_sent) = { + let row = context.sql.fetch_one( + sqlx::query( + "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;" + ) + .bind(chat_id) + ).await?; - Ok((send_begin, send_until, last_sent)) - }) - .await?; + let send_begin: i64 = row.try_get(0)?; + let send_until: i64 = row.try_get(1)?; + let last_sent: i64 = row.try_get(2)?; + + (send_begin, send_until, last_sent) + }; let now = time(); let mut location_count = 0; @@ -435,40 +438,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32) self_addr, ); - context.sql.query_map( - "SELECT id, latitude, longitude, accuracy, timestamp \ + let mut rows = context.sql.fetch( + sqlx::query( + "SELECT id, latitude, longitude, accuracy, timestamp \ FROM locations WHERE from_id=? \ AND timestamp>=? \ AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND independent=0 \ GROUP BY timestamp \ - ORDER BY timestamp;", - paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], - |row| { - let location_id: i32 = row.get(0)?; - let latitude: f64 = row.get(1)?; - let longitude: f64 = row.get(2)?; - let accuracy: f64 = row.get(3)?; - let timestamp = get_kml_timestamp(row.get(4)?); - - Ok((location_id, latitude, longitude, accuracy, timestamp)) - }, - |rows| { - for row in rows { - let (location_id, latitude, longitude, accuracy, timestamp) = row?; - ret += &format!( - "{}{},{}\n", - timestamp, - accuracy, - longitude, - latitude - ); - location_count += 1; - last_added_location_id = location_id as u32; - } - Ok(()) - } + ORDER BY timestamp;" + ) + .bind(DC_CONTACT_ID_SELF) + .bind(locations_send_begin) + .bind(locations_last_sent) + .bind(DC_CONTACT_ID_SELF) ).await?; + + while let Some(row) = rows.next().await { + let row = row?; + let location_id: u32 = row.try_get(0)?; + let latitude: f64 = row.try_get(1)?; + let longitude: f64 = row.try_get(2)?; + let accuracy: f64 = row.try_get(3)?; + let timestamp = get_kml_timestamp(row.try_get(4)?); + + ret += &format!( + "{}{},{}\n", + timestamp, + accuracy, + longitude, + latitude + ); + location_count += 1; + last_added_location_id = location_id; + } + ret += "\n"; } @@ -509,8 +513,9 @@ pub async fn set_kml_sent_timestamp( context .sql .execute( - "UPDATE chats SET locations_last_sent=? WHERE id=?;", - paramsv![timestamp, chat_id], + sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;") + .bind(timestamp) + .bind(chat_id), ) .await?; Ok(()) @@ -524,8 +529,9 @@ pub async fn set_msg_location_id( context .sql .execute( - "UPDATE msgs SET location_id=? WHERE id=?;", - paramsv![location_id, msg_id], + sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;") + .bind(location_id) + .bind(msg_id), ) .await?; @@ -544,6 +550,11 @@ pub async fn save( let mut newest_timestamp = 0; let mut newest_location_id = 0; + let stmt_test = "SELECT COUNT(*) FROM locations WHERE timestamp=? AND from_id=?"; + let stmt_insert = "INSERT INTO locations\ + (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ + VALUES (?,?,?,?,?,?,?);"; + for location in locations { let &Location { timestamp, @@ -552,53 +563,42 @@ pub async fn save( accuracy, .. } = location; - let (loc_id, ts) = context + let exists = context .sql - .with_conn(move |mut conn| { - let mut stmt_test = conn - .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?; - let mut stmt_insert = conn.prepare_cached( - "INSERT INTO locations\ - (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ - VALUES (?,?,?,?,?,?,?);", - )?; - - let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?; - - if independent || !exists { - stmt_insert.execute(paramsv![ - timestamp, - contact_id as i32, - chat_id, - latitude, - longitude, - accuracy, - independent, - ])?; - - if timestamp > newest_timestamp { - // okay to drop, as we use cached prepared statements - drop(stmt_test); - drop(stmt_insert); - newest_timestamp = timestamp; - newest_location_id = crate::sql::get_rowid2( - &mut conn, - "locations", - "timestamp", - timestamp, - "from_id", - contact_id as i32, - )?; - } - } - Ok((newest_location_id, newest_timestamp)) - }) + .exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id)) .await?; - newest_timestamp = ts; - newest_location_id = loc_id; + if independent || !exists { + context + .sql + .execute( + sqlx::query(stmt_insert) + .bind(timestamp) + .bind(contact_id) + .bind(chat_id) + .bind(latitude) + .bind(longitude) + .bind(accuracy) + .bind(independent), + ) + .await?; + + if timestamp > newest_timestamp { + newest_timestamp = timestamp; + newest_location_id = context + .sql + .get_rowid2( + "locations", + "timestamp", + timestamp, + "from_id", + contact_id as i64, + ) + .await?; + } + } } - Ok(newest_location_id) + Ok(u32::try_from(newest_location_id)?) } pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status { @@ -611,15 +611,21 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j let rows = context .sql - .query_map( - "SELECT id, locations_send_begin, locations_last_sent \ + .fetch( + sqlx::query( + "SELECT id, locations_send_begin, locations_last_sent \ FROM chats \ WHERE locations_send_until>?;", - paramsv![now], - |row| { - let chat_id: ChatId = row.get(0)?; - let locations_send_begin: i64 = row.get(1)?; - let locations_last_sent: i64 = row.get(2)?; + ) + .bind(now), + ) + .await + .map(|rows| { + rows.map(|row| -> sqlx::Result> { + let row = row?; + let chat_id: ChatId = row.try_get(0)?; + let locations_send_begin: i64 = row.try_get(1)?; + let locations_last_sent: i64 = row.try_get(2)?; continue_streaming = true; // be a bit tolerant as the timer may not align exactly with time(NULL) @@ -628,64 +634,55 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j } else { Ok(Some((chat_id, locations_send_begin, locations_last_sent))) } - }, - |rows| { - rows.filter_map(|v| v.transpose()) - .collect::, _>>() - .map_err(Into::into) - }, - ) - .await; - - if rows.is_ok() { - let msgs = context - .sql - .with_conn(move |conn| { - let rows = rows.unwrap(); - - let mut stmt_locations = conn.prepare_cached( - "SELECT id \ - FROM locations \ - WHERE from_id=? \ - AND timestamp>=? \ - AND timestamp>? \ - AND independent=0 \ - ORDER BY timestamp;", - )?; - - let mut msgs = Vec::new(); - for (chat_id, locations_send_begin, locations_last_sent) in &rows { - if !stmt_locations - .exists(paramsv![ - DC_CONTACT_ID_SELF, - *locations_send_begin, - *locations_last_sent, - ]) - .unwrap_or_default() - { - // if there is no new location, there's nothing to send. - // however, maybe we want to bypass this test eg. 15 minutes - } else { - // pending locations are attached automatically to every message, - // so also to this empty text message. - // DC_CMD_LOCATION is only needed to create a nicer subject. - // - // for optimisation and to avoid flooding the sending queue, - // we could sending these messages only if we're really online. - // the easiest way to determine this, is to check for an empty message queue. - // (might not be 100%, however, as positions are sent combined later - // and dc_set_location() is typically called periodically, this is ok) - let mut msg = Message::new(Viewtype::Text); - msg.hidden = true; - msg.param.set_cmd(SystemMessage::LocationOnly); - msgs.push((*chat_id, msg)); - } - } - - Ok(msgs) }) - .await - .unwrap_or_default(); + .filter_map(|v| v.transpose()) + }); + + let stmt = "SELECT COUNT(*) \ + FROM locations \ + WHERE from_id=? \ + AND timestamp>=? \ + AND timestamp>? \ + AND independent=0 \ + ORDER BY timestamp;"; + + if let Ok(mut rows) = rows { + let mut msgs = Vec::new(); + while let Some(row) = rows.next().await { + let (chat_id, locations_send_begin, locations_last_sent) = match row { + Ok(row) => row, + Err(_) => break, + }; + let exists = context + .sql + .exists( + sqlx::query(stmt) + .bind(DC_CONTACT_ID_SELF) + .bind(locations_send_begin) + .bind(locations_last_sent), + ) + .await + .unwrap_or_default(); // TODO: better error handling + + if !exists { + // if there is no new location, there's nothing to send. + // however, maybe we want to bypass this test eg. 15 minutes + } else { + // pending locations are attached automatically to every message, + // so also to this empty text message. + // DC_CMD_LOCATION is only needed to create a nicer subject. + // + // for optimisation and to avoid flooding the sending queue, + // we could sending these messages only if we're really online. + // the easiest way to determine this, is to check for an empty message queue. + // (might not be 100%, however, as positions are sent combined later + // and dc_set_location() is typically called periodically, this is ok) + let mut msg = Message::new(Viewtype::Text); + msg.hidden = true; + msg.param.set_cmd(SystemMessage::LocationOnly); + msgs.push((chat_id, msg)); + } + } for (chat_id, mut msg) in msgs.into_iter() { // TODO: better error handling @@ -711,16 +708,16 @@ pub(crate) async fn job_maybe_send_locations_ended( let chat_id = ChatId::new(job.foreign_id); - let (send_begin, send_until) = job_try!( - context - .sql - .query_row( + let (send_begin, send_until) = job_try!(context + .sql + .fetch_one( + sqlx::query( "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", - paramsv![chat_id], - |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), ) - .await - ); + .bind(chat_id) + ) + .await + .and_then(|row| { Ok((row.try_get::(0)?, row.try_get::(1)?)) })); if !(send_begin != 0 && time() <= send_until) { // still streaming - @@ -728,10 +725,19 @@ pub(crate) async fn job_maybe_send_locations_ended( // do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs if !(send_begin == 0 && send_until == 0) { // not streaming, device-message already sent - job_try!(context.sql.execute( - "UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?", - paramsv![chat_id], - ).await); + job_try!( + context + .sql + .execute( + sqlx::query( + "UPDATE chats \ + SET locations_send_begin=0, locations_send_until=0 \ + WHERE id=?" + ) + .bind(chat_id) + ) + .await + ); let stock_str = stock_str::msg_location_disabled(context).await; chat::add_info_msg(context, chat_id, stock_str).await; diff --git a/src/login_param.rs b/src/login_param.rs index 6c53f2cf6..2ceb8cd2f 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -7,7 +7,7 @@ use crate::provider::{get_provider_by_id, Provider}; use crate::{context::Context, provider::Socket}; #[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)] -#[repr(i32)] +#[repr(u32)] #[strum(serialize_all = "snake_case")] pub enum CertificateChecks { /// Same as AcceptInvalidCertificates unless overridden by @@ -54,91 +54,85 @@ pub struct LoginParam { impl LoginParam { /// Read the login parameters from the database. - pub async fn from_database(context: &Context, prefix: impl AsRef) -> Self { + pub async fn from_database( + context: &Context, + prefix: impl AsRef, + ) -> crate::sql::Result { let prefix = prefix.as_ref(); let sql = &context.sql; let key = format!("{}addr", prefix); let addr = sql - .get_raw_config(context, key) - .await + .get_raw_config(key) + .await? .unwrap_or_default() .trim() .to_string(); let key = format!("{}mail_server", prefix); - let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default(); + let mail_server = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}mail_port", prefix); - let mail_port = sql - .get_raw_config_int(context, key) - .await - .unwrap_or_default(); + let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); let key = format!("{}mail_user", prefix); - let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default(); + let mail_user = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}mail_pw", prefix); - let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default(); + let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}mail_security", prefix); let mail_security = sql - .get_raw_config_int(context, key) - .await + .get_raw_config_int(key) + .await? .and_then(num_traits::FromPrimitive::from_i32) .unwrap_or_default(); let key = format!("{}imap_certificate_checks", prefix); let imap_certificate_checks = - if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await { + if let Some(certificate_checks) = sql.get_raw_config_int(key).await? { num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() } else { Default::default() }; let key = format!("{}send_server", prefix); - let send_server = sql.get_raw_config(context, key).await.unwrap_or_default(); + let send_server = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}send_port", prefix); - let send_port = sql - .get_raw_config_int(context, key) - .await - .unwrap_or_default(); + let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); let key = format!("{}send_user", prefix); - let send_user = sql.get_raw_config(context, key).await.unwrap_or_default(); + let send_user = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}send_pw", prefix); - let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default(); + let send_pw = sql.get_raw_config(key).await?.unwrap_or_default(); let key = format!("{}send_security", prefix); let send_security = sql - .get_raw_config_int(context, key) - .await + .get_raw_config_int(key) + .await? .and_then(num_traits::FromPrimitive::from_i32) .unwrap_or_default(); let key = format!("{}smtp_certificate_checks", prefix); let smtp_certificate_checks = - if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await { + if let Some(certificate_checks) = sql.get_raw_config_int(key).await? { num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() } else { Default::default() }; let key = format!("{}server_flags", prefix); - let server_flags = sql - .get_raw_config_int(context, key) - .await - .unwrap_or_default(); + let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default(); let key = format!("{}provider", prefix); let provider = sql - .get_raw_config(context, key) - .await + .get_raw_config(key) + .await? .and_then(|provider_id| get_provider_by_id(&provider_id)); - LoginParam { + Ok(LoginParam { addr, imap: ServerLoginParam { server: mail_server, @@ -158,7 +152,7 @@ impl LoginParam { }, provider, server_flags, - } + }) } /// Save this loginparam to the database. @@ -171,63 +165,54 @@ impl LoginParam { let sql = &context.sql; let key = format!("{}addr", prefix); - sql.set_raw_config(context, key, Some(&self.addr)).await?; + sql.set_raw_config(key, Some(&self.addr)).await?; let key = format!("{}mail_server", prefix); - sql.set_raw_config(context, key, Some(&self.imap.server)) - .await?; + sql.set_raw_config(key, Some(&self.imap.server)).await?; let key = format!("{}mail_port", prefix); - sql.set_raw_config_int(context, key, self.imap.port as i32) - .await?; + sql.set_raw_config_int(key, self.imap.port as i32).await?; let key = format!("{}mail_user", prefix); - sql.set_raw_config(context, key, Some(&self.imap.user)) - .await?; + sql.set_raw_config(key, Some(&self.imap.user)).await?; let key = format!("{}mail_pw", prefix); - sql.set_raw_config(context, key, Some(&self.imap.password)) - .await?; + sql.set_raw_config(key, Some(&self.imap.password)).await?; let key = format!("{}mail_security", prefix); - sql.set_raw_config_int(context, key, self.imap.security as i32) + sql.set_raw_config_int(key, self.imap.security as i32) .await?; let key = format!("{}imap_certificate_checks", prefix); - sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32) + sql.set_raw_config_int(key, self.imap.certificate_checks as i32) .await?; let key = format!("{}send_server", prefix); - sql.set_raw_config(context, key, Some(&self.smtp.server)) - .await?; + sql.set_raw_config(key, Some(&self.smtp.server)).await?; let key = format!("{}send_port", prefix); - sql.set_raw_config_int(context, key, self.smtp.port as i32) - .await?; + sql.set_raw_config_int(key, self.smtp.port as i32).await?; let key = format!("{}send_user", prefix); - sql.set_raw_config(context, key, Some(&self.smtp.user)) - .await?; + sql.set_raw_config(key, Some(&self.smtp.user)).await?; let key = format!("{}send_pw", prefix); - sql.set_raw_config(context, key, Some(&self.smtp.password)) - .await?; + sql.set_raw_config(key, Some(&self.smtp.password)).await?; let key = format!("{}send_security", prefix); - sql.set_raw_config_int(context, key, self.smtp.security as i32) + sql.set_raw_config_int(key, self.smtp.security as i32) .await?; let key = format!("{}smtp_certificate_checks", prefix); - sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32) + sql.set_raw_config_int(key, self.smtp.certificate_checks as i32) .await?; let key = format!("{}server_flags", prefix); - sql.set_raw_config_int(context, key, self.server_flags) - .await?; + sql.set_raw_config_int(key, self.server_flags).await?; if let Some(provider) = self.provider { let key = format!("{}provider", prefix); - sql.set_raw_config(context, key, Some(provider.id)).await?; + sql.set_raw_config(key, Some(provider.id)).await?; } Ok(()) diff --git a/src/lot.rs b/src/lot.rs index e209bbd80..4a132173c 100644 --- a/src/lot.rs +++ b/src/lot.rs @@ -1,5 +1,3 @@ -use deltachat_derive::{FromSql, ToSql}; - use crate::key::Fingerprint; /// An object containing a set of values. @@ -22,9 +20,7 @@ pub struct Lot { } #[repr(u8)] -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] pub enum Meaning { None = 0, Text1Draft = 1, @@ -68,10 +64,8 @@ impl Lot { } } -#[repr(i32)] -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, -)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[repr(u32)] pub enum LotState { // Default Undefined = 0, diff --git a/src/message.rs b/src/message.rs index 411ec14ec..ea8e48c51 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2,9 +2,10 @@ use anyhow::{ensure, Error}; use async_std::path::{Path, PathBuf}; -use deltachat_derive::{FromSql, ToSql}; +use async_std::prelude::*; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use sqlx::Row; use crate::chat::{self, Chat, ChatId}; use crate::config::Config; @@ -40,8 +41,20 @@ const SUMMARY_CHARACTERS: usize = 160; /// This type can represent both the special as well as normal /// messages. #[derive( - Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, + Debug, + Copy, + Clone, + Default, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Serialize, + Deserialize, + sqlx::Type, )] +#[sqlx(transparent)] pub struct MsgId(u32); impl MsgId { @@ -79,7 +92,7 @@ impl MsgId { pub async fn get_state(self, context: &Context) -> crate::sql::Result { let result = context .sql - .query_get_value_result("SELECT state FROM msgs WHERE id=?", paramsv![self]) + .query_get_value(sqlx::query("SELECT state FROM msgs WHERE id=?").bind(self)) .await? .unwrap_or_default(); Ok(result) @@ -94,13 +107,13 @@ impl MsgId { folder: &str, ) -> Result, Error> { use Config::*; - if context.is_mvbox(folder).await { + if context.is_mvbox(folder).await? { return Ok(None); } let msg = Message::load_from_db(context, self).await?; - if context.is_spam_folder(folder).await { + if context.is_spam_folder(folder).await? { return if msg.chat_blocked == Blocked::Not { if self.needs_move_to_mvbox(context, &msg).await? { Ok(Some(ConfiguredMvboxFolder)) @@ -119,9 +132,9 @@ impl MsgId { && msg.is_dc_message == MessengerMessage::Yes && !msg.is_setupmessage() && msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this - && context.is_inbox(folder).await - && context.get_config_bool(SentboxMove).await - && context.get_config(ConfiguredSentboxFolder).await.is_some() + && context.is_inbox(folder).await? + && context.get_config_bool(SentboxMove).await? + && context.get_config(ConfiguredSentboxFolder).await?.is_some() { Ok(Some(ConfiguredSentboxFolder)) } else { @@ -130,7 +143,7 @@ impl MsgId { } async fn needs_move_to_mvbox(self, context: &Context, msg: &Message) -> Result { - if !context.get_config_bool(Config::MvboxMove).await { + if !context.get_config_bool(Config::MvboxMove).await? { return Ok(false); } @@ -153,14 +166,26 @@ impl MsgId { /// 1. not download the same message again /// 2. be able to delete the message on the server if we want to pub async fn trash(self, context: &Context) -> crate::sql::Result<()> { - let chat_id = ChatId::new(DC_CHAT_ID_TRASH); + let chat_id = DC_CHAT_ID_TRASH; context .sql .execute( - // If you change which information is removed here, also change delete_expired_messages() and - // which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH - "UPDATE msgs SET chat_id=?, txt='', subject='', txt_raw='', mime_headers='', from_id=0, to_id=0, param='' WHERE id=?", - paramsv![chat_id, self], + sqlx::query( + // If you change which information is removed here, also change delete_expired_messages() and + // which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH + r#" +UPDATE msgs +SET + chat_id=?, txt='', + subject='', txt_raw='', + mime_headers='', + from_id=0, to_id=0, + param='' +WHERE id=?; +"#, + ) + .bind(chat_id) + .bind(self), ) .await?; @@ -173,11 +198,11 @@ impl MsgId { // sure they are not left while the message is deleted. context .sql - .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self]) + .execute(sqlx::query("DELETE FROM msgs_mdns WHERE msg_id=?;").bind(self)) .await?; context .sql - .execute("DELETE FROM msgs WHERE id=?;", paramsv![self]) + .execute(sqlx::query("DELETE FROM msgs WHERE id=?;").bind(self)) .await?; Ok(()) } @@ -191,10 +216,12 @@ impl MsgId { context .sql .execute( - "UPDATE msgs \ + sqlx::query( + "UPDATE msgs \ SET server_folder='', server_uid=0 \ WHERE id=?", - paramsv![self], + ) + .bind(self), ) .await?; Ok(()) @@ -215,41 +242,6 @@ impl std::fmt::Display for MsgId { } } -/// Allow converting [MsgId] to an SQLite type. -/// -/// This allows you to directly store [MsgId] into the database. -/// -/// # Errors -/// -/// This **does** ensure that no special message IDs are written into -/// the database and the conversion will fail if this is not the case. -impl rusqlite::types::ToSql for MsgId { - fn to_sql(&self) -> rusqlite::Result { - if self.0 <= DC_MSG_ID_LAST_SPECIAL { - return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( - InvalidMsgId, - ))); - } - let val = rusqlite::types::Value::Integer(self.0 as i64); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - -/// Allow converting an SQLite integer directly into [MsgId]. -impl rusqlite::types::FromSql for MsgId { - fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { - // Would be nice if we could use match here, but alas. - i64::column_result(value).and_then(|val| { - if 0 <= val && val <= std::u32::MAX as i64 { - Ok(MsgId::new(val as u32)) - } else { - Err(rusqlite::types::FromSqlError::OutOfRange(val)) - } - }) - } -} - /// Message ID was invalid. /// /// This usually occurs when trying to use a message ID of @@ -260,18 +252,9 @@ impl rusqlite::types::FromSql for MsgId { pub struct InvalidMsgId; #[derive( - Debug, - Copy, - Clone, - PartialEq, - FromPrimitive, - ToPrimitive, - FromSql, - ToSql, - Serialize, - Deserialize, + Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, Serialize, Deserialize, sqlx::Type, )] -#[repr(u8)] +#[repr(i8)] pub(crate) enum MessengerMessage { No = 0, Yes = 1, @@ -334,10 +317,10 @@ impl Message { !id.is_special(), "Can not load special message IDs from DB." ); - let msg = context + let row = context .sql - .query_row( - concat!( + .fetch_one( + sqlx::query(concat!( "SELECT", " m.id AS id,", " rfc724_mid AS rfc724mid,", @@ -365,62 +348,63 @@ impl Message { " c.blocked AS blocked", " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", " WHERE m.id=?;" - ), - paramsv![id], - |row| { - let text = match row.get_raw("txt") { - rusqlite::types::ValueRef::Text(buf) => { - match String::from_utf8(buf.to_vec()) { - Ok(t) => t, - Err(_) => { - warn!( - context, - concat!( - "dc_msg_load_from_db: could not get ", - "text column as non-lossy utf8 id {}" - ), - id - ); - String::from_utf8_lossy(buf).into_owned() - } - } - } - _ => String::new(), - }; - let msg = Message { - id: row.get("id")?, - rfc724_mid: row.get::<_, String>("rfc724mid")?, - in_reply_to: row.get::<_, Option>("mime_in_reply_to")?, - server_folder: row.get::<_, Option>("server_folder")?, - server_uid: row.get("server_uid")?, - chat_id: row.get("chat_id")?, - from_id: row.get("from_id")?, - to_id: row.get("to_id")?, - timestamp_sort: row.get("timestamp")?, - timestamp_sent: row.get("timestamp_sent")?, - timestamp_rcvd: row.get("timestamp_rcvd")?, - ephemeral_timer: row.get("ephemeral_timer")?, - ephemeral_timestamp: row.get("ephemeral_timestamp")?, - viewtype: row.get("type")?, - state: row.get("state")?, - error: Some(row.get::<_, String>("error")?) - .filter(|error| !error.is_empty()), - is_dc_message: row.get("msgrmsg")?, - mime_modified: row.get("mime_modified")?, - text: Some(text), - subject: row.get("subject")?, - param: row.get::<_, String>("param")?.parse().unwrap_or_default(), - hidden: row.get("hidden")?, - location_id: row.get("location")?, - chat_blocked: row - .get::<_, Option>("blocked")? - .unwrap_or_default(), - }; - Ok(msg) - }, + )) + .bind(id), ) .await?; + let text; + if let Ok(Some(buf)) = row.try_get::, _>("txt") { + if let Ok(t) = String::from_utf8(buf.to_vec()) { + text = t; + } else { + warn!( + context, + concat!( + "dc_msg_load_from_db: could not get ", + "text column as non-lossy utf8 id {}" + ), + id + ); + text = String::from_utf8_lossy(buf).into_owned(); + } + } else { + text = "".to_string(); + } + + let msg = Message { + id: row.try_get("id")?, + rfc724_mid: row.try_get("rfc724mid")?, + in_reply_to: row.try_get("mime_in_reply_to")?, + server_folder: row.try_get("server_folder")?, + server_uid: row.try_get("server_uid")?, + chat_id: row.try_get("chat_id")?, + from_id: row.try_get("from_id")?, + to_id: row.try_get("to_id")?, + timestamp_sort: row.try_get("timestamp")?, + timestamp_sent: row.try_get("timestamp_sent")?, + timestamp_rcvd: row.try_get("timestamp_rcvd")?, + ephemeral_timer: row.try_get("ephemeral_timer")?, + ephemeral_timestamp: row.try_get("ephemeral_timestamp")?, + viewtype: row.try_get("type")?, + state: row.try_get("state")?, + error: row + .try_get::, _>("error")? + .filter(|e| !e.is_empty()), + is_dc_message: row.try_get("msgrmsg")?, + mime_modified: row.try_get("mime_modified")?, + subject: row.try_get("subject")?, + param: row + .try_get::("param")? + .parse() + .unwrap_or_default(), + hidden: row.try_get("hidden")?, + location_id: row.try_get("location")?, + chat_blocked: row + .try_get::, _>("blocked")? + .unwrap_or_default(), + text: Some(text), + }; Ok(msg) } @@ -520,7 +504,7 @@ impl Message { /// if the message is a contact request, the DC_CHAT_ID_DEADDROP is returned. pub fn get_chat_id(&self) -> ChatId { if self.chat_blocked != Blocked::Not { - ChatId::new(DC_CHAT_ID_DEADDROP) + DC_CHAT_ID_DEADDROP } else { self.chat_id } @@ -610,7 +594,7 @@ impl Message { return ret; }; - let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 { + let contact = if self.from_id != DC_CONTACT_ID_SELF { match chat.typ { Chattype::Group | Chattype::Mailinglist => { Contact::get_by_id(context, self.from_id).await.ok() @@ -678,8 +662,8 @@ impl Message { pub fn is_info(&self) -> bool { let cmd = self.param.get_cmd(); - self.from_id == DC_CONTACT_ID_INFO as u32 - || self.to_id == DC_CONTACT_ID_INFO as u32 + self.from_id == DC_CONTACT_ID_INFO + || self.to_id == DC_CONTACT_ID_INFO || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage } @@ -916,8 +900,9 @@ impl Message { context .sql .execute( - "UPDATE msgs SET param=? WHERE id=?;", - paramsv![self.param.to_string(), self.id], + sqlx::query("UPDATE msgs SET param=? WHERE id=?;") + .bind(self.param.to_string()) + .bind(self.id), ) .await .ok_or_log(context); @@ -927,8 +912,9 @@ impl Message { context .sql .execute( - "UPDATE msgs SET subject=? WHERE id=?;", - paramsv![self.subject, self.id], + sqlx::query("UPDATE msgs SET subject=? WHERE id=?;") + .bind(&self.subject) + .bind(&self.id), ) .await .ok_or_log(context); @@ -966,12 +952,11 @@ pub enum ContactRequestDecision { Eq, FromPrimitive, ToPrimitive, - ToSql, - FromSql, Serialize, Deserialize, + sqlx::Type, )] -#[repr(i32)] +#[repr(u32)] pub enum MessageState { Undefined = 0, @@ -1208,28 +1193,18 @@ pub async fn decide_on_contact_request( created_chat_id } -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).await; - if msg.is_err() { - return ret; - } - - let msg = msg.unwrap_or_default(); - +pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { + let msg = Message::load_from_db(context, msg_id).await?; let rawtxt: Option = context .sql - .query_get_value( - context, - "SELECT txt_raw FROM msgs WHERE id=?;", - paramsv![msg_id], - ) - .await; + .query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id)) + .await?; + + let mut ret = String::new(); if rawtxt.is_none() { ret += &format!("Cannot load message {}.", msg_id); - return ret; + return Ok(ret); } let rawtxt = rawtxt.unwrap_or_default(); let rawtxt = dc_truncate(rawtxt.trim(), DC_MAX_GET_INFO_LEN); @@ -1245,7 +1220,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String { ret += &format!(" by {}", name); ret += "\n"; - if msg.from_id != DC_CONTACT_ID_SELF as u32 { + if msg.from_id != DC_CONTACT_ID_SELF { let s = dc_timestamp_to_str(if 0 != msg.timestamp_rcvd { msg.timestamp_rcvd } else { @@ -1268,28 +1243,32 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String { if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO { // device-internal message, no further details needed - return ret; + return Ok(ret); } - if let Ok(rows) = context + if let Ok(mut 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), + .fetch( + sqlx::query("SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;") + .bind(msg_id), ) .await + .map(|rows| { + rows.map(|row| -> sqlx::Result<_> { + let row = row?; + let contact_id = row.try_get(0)?; + let ts = row.try_get(1)?; + Ok((contact_id, ts)) + }) + }) { - for (contact_id, ts) in rows { + while let Some(row) = rows.next().await { + let (contact_id, ts) = row?; + let fts = dc_timestamp_to_str(ts); ret += &format!("Read: {}", fts); - let name = Contact::load_from_db(context, contact_id as u32) + let name = Contact::load_from_db(context, contact_id) .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default(); @@ -1353,7 +1332,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String { } } - ret + Ok(ret) } pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { @@ -1431,15 +1410,21 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { Some(info) } -pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option { - context +/// Get the raw mime-headers of the given message. +/// Raw headers are saved for incoming messages +/// only if `dc_set_config(context, "save_mime_headers", "1")` +/// was called before. +/// +/// Returns an empty string if there are no headers saved for the given message, +/// e.g. because of save_mime_headers is not set +/// or the message is not incoming. +pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result { + let headers = context .sql - .query_get_value( - context, - "SELECT mime_headers FROM msgs WHERE id=?;", - paramsv![msg_id], - ) - .await + .query_get_value(sqlx::query("SELECT mime_headers FROM msgs WHERE id=?;").bind(msg_id)) + .await? + .unwrap_or_default(); + Ok(headers) } pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) { @@ -1477,8 +1462,7 @@ 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], + sqlx::query("DELETE FROM locations WHERE independent = 1 AND id=?;").bind(location_id), ) .await .is_ok() @@ -1488,40 +1472,39 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> bool { if msg_ids.is_empty() { return false; } - - let msgs = context - .sql - .with_conn(move |conn| { - let mut stmt = conn.prepare_cached(concat!( - "SELECT", - " m.chat_id AS chat_id,", - " 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::<_, ChatId>("chat_id")?, - row.get::<_, MessageState>("state")?, - row.get::<_, Option>("blocked")? + let stmt = concat!( + "SELECT", + " m.chat_id AS chat_id,", + " 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() { + match context + .sql + .fetch_optional(sqlx::query(stmt).bind(id)) + .await + .and_then(|row| { + if let Some(row) = row { + Ok(Some(( + row.try_get::("chat_id")?, + row.try_get::("state")?, + row.try_get::, _>("blocked")? .unwrap_or_default(), - )) - }); - if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res { - continue; + ))) + } else { + Ok(None) } - let (chat_id, state, blocked) = query_res.map_err(Into::::into)?; - msgs.push((id, chat_id, state, blocked)); + }) { + Ok(Some((chat_id, state, blocked))) => msgs.push((id, chat_id, state, blocked)), + Ok(None) => {} + Err(err) => { + warn!(context, "failed to markseen msgs: {:?}", err); } - - Ok(msgs) - }) - .await - .unwrap_or_default(); + } + } let mut updated_chat_ids = BTreeMap::new(); @@ -1548,7 +1531,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> bool { } } else if curr_state == MessageState::InFresh { update_msg_state(context, id, MessageState::InNoticed).await; - updated_chat_ids.insert(ChatId::new(DC_CHAT_ID_DEADDROP), true); + updated_chat_ids.insert(DC_CHAT_ID_DEADDROP, true); } } @@ -1563,8 +1546,9 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt context .sql .execute( - "UPDATE msgs SET state=? WHERE id=?;", - paramsv![state, msg_id], + sqlx::query("UPDATE msgs SET state=? WHERE id=?;") + .bind(state) + .bind(msg_id), ) .await .is_ok() @@ -1647,24 +1631,20 @@ pub async fn get_summarytext_by_raw( // Context functions to work with messages -pub async fn exists(context: &Context, msg_id: MsgId) -> bool { +pub async fn exists(context: &Context, msg_id: MsgId) -> anyhow::Result { if msg_id.is_special() { - return false; + return Ok(false); } let chat_id: Option = context .sql - .query_get_value( - context, - "SELECT chat_id FROM msgs WHERE id=?;", - paramsv![msg_id], - ) - .await; + .query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?;").bind(msg_id)) + .await?; if let Some(chat_id) = chat_id { - !chat_id.is_trash() + Ok(!chat_id.is_trash()) } else { - false + Ok(false) } } @@ -1684,8 +1664,10 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option Option<(ChatId, MsgId)> { +) -> anyhow::Result> { if from_id <= DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid.is_empty() { - return None; + return Ok(None); } let res = context .sql - .query_row( - concat!( + .fetch_one( + sqlx::query(concat!( "SELECT", " m.id AS msg_id,", " c.id AS chat_id,", @@ -1723,18 +1705,19 @@ pub async fn handle_mdn( " 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")?, - )) - }, + )) + .bind(rfc724_mid), ) - .await; + .await + .and_then(|row| { + Ok(( + row.try_get::("msg_id")?, + row.try_get::("chat_id")?, + row.try_get::("type")?, + row.try_get::("state")?, + )) + }); + if let Err(ref err) = res { info!(context, "Failed to select MDN {:?}", err); } @@ -1749,16 +1732,19 @@ pub async fn handle_mdn( let mdn_already_in_table = context .sql .exists( - "SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;", - paramsv![msg_id, from_id as i32,], + sqlx::query("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;") + .bind(msg_id) + .bind(from_id), ) .await .unwrap_or_default(); if !mdn_already_in_table { context.sql.execute( - "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);", - paramsv![msg_id, from_id as i32, timestamp_sent], + sqlx::query("INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);") + .bind(msg_id) + .bind(from_id) + .bind(timestamp_sent) ) .await .unwrap_or_default(); // TODO: better error handling @@ -1772,27 +1758,23 @@ pub async fn handle_mdn( // send event about new state let ist_cnt = context .sql - .query_get_value::( - context, - "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;", - paramsv![msg_id], + .count( + sqlx::query("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;").bind(msg_id), ) - .await - .unwrap_or_default() as usize; - /* - Groupsize: Min. MDNs + .await?; - 1 S n/a - 2 SR 1 - 3 SRR 2 - 4 SRRR 2 - 5 SRRRR 3 - 6 SRRRRR 3 + // Groupsize: Min. MDNs + // 1 S n/a + // 2 SR 1 + // 3 SRR 2 + // 4 SRRR 2 + // 5 SRRRR 3 + // 6 SRRRRR 3 + // + // (S=Sender, R=Recipient) - (S=Sender, R=Recipient) - */ // for rounding, SELF is already included! - let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id).await + 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).await; read_by_all = true; @@ -1800,12 +1782,12 @@ pub async fn handle_mdn( } } return if read_by_all { - Some((chat_id, msg_id)) + Ok(Some((chat_id, msg_id))) } else { - None + Ok(None) }; } - None + Ok(None) } /// Marks a message as failed after an ndn (non-delivery-notification) arrived. @@ -1821,36 +1803,34 @@ pub(crate) async fn handle_ndn( // The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client. // In this case we need to mark multiple "msgids" as failed that all refer to the same message-id. - let msgs: Vec<_> = context + let mut rows = context .sql - .query_map( - concat!( + .fetch( + sqlx::query(concat!( "SELECT", " m.id AS msg_id,", " c.id AS chat_id,", " c.type AS type", " FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id", " WHERE rfc724_mid=? AND from_id=1", - ), - paramsv![failed.rfc724_mid], - |row| { - Ok(( - row.get::<_, MsgId>("msg_id")?, - row.get::<_, ChatId>("chat_id")?, - row.get::<_, Chattype>("type")?, - )) - }, - |rows| Ok(rows.collect::>()), + )) + .bind(&failed.rfc724_mid), ) .await?; - for (i, msg) in msgs.into_iter().enumerate() { - let (msg_id, chat_id, chat_type) = msg?; + let mut first = true; + while let Some(row) = rows.next().await { + let row = row?; + let msg_id = row.try_get::("msg_id")?; + let chat_id = row.try_get::("chat_id")?; + let chat_type = row.try_get::("type")?; + set_msg_failed(context, msg_id, error.as_ref()).await; - if i == 0 { + if first { // Add only one info msg for all failed messages ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?; } + first = false; } Ok(()) @@ -1890,15 +1870,13 @@ async fn ndn_maybe_add_info_msg( } /// The number of messages assigned to real chat (!=deaddrop, !=trash) -pub async fn get_real_msg_cnt(context: &Context) -> i32 { +pub async fn get_real_msg_cnt(context: &Context) -> usize { match context .sql - .query_row( + .count( "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;", - paramsv![], - |row| row.get(0), ) .await { @@ -1913,16 +1891,14 @@ pub async fn get_real_msg_cnt(context: &Context) -> i32 { pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize { match context .sql - .query_row( + .count( "SELECT COUNT(*) \ FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ WHERE c.blocked=2;", - paramsv![], - |row| row.get::<_, isize>(0), ) .await { - Ok(res) => res as usize, + Ok(res) => res, Err(err) => { error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err); 0 @@ -1941,55 +1917,56 @@ pub async fn estimate_deletion_cnt( .0; let threshold_timestamp = time() - seconds; - let cnt: isize = if from_server { + let cnt = if from_server { context .sql - .query_row( - "SELECT COUNT(*) + .count( + sqlx::query( + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND server_uid != 0;", - paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], - |row| row.get(0), + ) + .bind(DC_MSG_ID_LAST_SPECIAL) + .bind(threshold_timestamp) + .bind(self_chat_id), ) .await? } else { context .sql - .query_row( - "SELECT COUNT(*) + .count( + sqlx::query( + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND chat_id != ? AND hidden = 0;", - paramsv![ - DC_MSG_ID_LAST_SPECIAL, - threshold_timestamp, - self_chat_id, - ChatId::new(DC_CHAT_ID_TRASH) - ], - |row| row.get(0), + ) + .bind(DC_MSG_ID_LAST_SPECIAL) + .bind(threshold_timestamp) + .bind(self_chat_id) + .bind(DC_CHAT_ID_TRASH), ) .await? }; - Ok(cnt as usize) + Ok(cnt) } /// Counts number of database records pointing to specified /// Message-ID. /// /// Unlinked messages are excluded. -pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 { +pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> usize { // 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", - paramsv![rfc724_mid], - |row| row.get(0), + .count( + sqlx::query("SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0") + .bind(rfc724_mid), ) .await { @@ -2011,22 +1988,22 @@ pub(crate) async fn rfc724_mid_exists( return Ok(None); } - let res = context + let row = context .sql - .query_row_optional( - "SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?", - paramsv![rfc724_mid], - |row| { - let server_folder = row.get::<_, Option>(0)?.unwrap_or_default(); - let server_uid = row.get(1)?; - let msg_id: MsgId = row.get(2)?; - - Ok((server_folder, server_uid, msg_id)) - }, + .fetch_optional( + sqlx::query("SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?") + .bind(rfc724_mid), ) .await?; + if let Some(row) = row { + let server_folder = row.try_get::, _>(0)?.unwrap_or_default(); + let server_uid = row.try_get::(1)?; + let msg_id: MsgId = row.try_get(2)?; - Ok(res) + Ok(Some((server_folder, server_uid, msg_id))) + } else { + Ok(None) + } } pub async fn update_server_uid( @@ -2038,9 +2015,13 @@ pub async fn update_server_uid( match context .sql .execute( - "UPDATE msgs SET server_folder=?, server_uid=? \ + sqlx::query( + "UPDATE msgs SET server_folder=?, server_uid=? \ WHERE rfc724_mid=?", - paramsv![server_folder.as_ref(), server_uid, rfc724_mid], + ) + .bind(server_folder.as_ref()) + .bind(server_uid as i64) + .bind(rfc724_mid), ) .await { @@ -2259,7 +2240,7 @@ mod tests { let msg = t.get_last_msg().await; let actual = if let Some(config) = msg.id.needs_move(&t.ctx, folder).await.unwrap() { - Some(t.ctx.get_config(config).await.unwrap()) + t.ctx.get_config(config).await.unwrap() } else { None }; @@ -2479,7 +2460,9 @@ mod tests { .unwrap(); let mut has_image = false; - let chatitems = chat::get_chat_msgs(&t, device_chat_id, 0, None).await; + let chatitems = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap(); for chatitem in chatitems { if let ChatItem::Message { msg_id } = chatitem { if let Ok(msg) = Message::load_from_db(&t, msg_id).await { @@ -2568,6 +2551,7 @@ mod tests { let chat = alice.create_chat(&bob).await; let contact_id = *chat::get_chat_contacts(&alice, chat.id) .await + .unwrap() .first() .unwrap(); let contact = Contact::load_from_db(&alice, contact_id).await.unwrap(); @@ -2587,6 +2571,7 @@ mod tests { let chat = bob.create_chat(&alice).await; let contact_id = *chat::get_chat_contacts(&bob, chat.id) .await + .unwrap() .first() .unwrap(); let contact = Contact::load_from_db(&bob, contact_id).await.unwrap(); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 581d7b1f6..b2fba251d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,3 +1,11 @@ +use std::convert::TryInto; + +use anyhow::{bail, ensure, format_err, Error}; +use async_std::prelude::*; +use chrono::TimeZone; +use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; +use sqlx::Row; + use crate::blob::BlobObject; use crate::chat::{self, Chat}; use crate::config::Config; @@ -20,11 +28,6 @@ use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::simplify::escape_message_footer_marks; use crate::stock_str; -use anyhow::Context as _; -use anyhow::{bail, ensure, format_err, Error}; -use chrono::TimeZone; -use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; -use std::convert::TryInto; // attachments of 25 mb brutto should work on the majority of providers // (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100). @@ -92,12 +95,12 @@ impl<'a> MimeFactory<'a> { let from_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); let config_displayname = context .get_config(Config::Displayname) - .await + .await? .unwrap_or_default(); let (from_displayname, sender_displayname) = if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) { @@ -112,52 +115,42 @@ impl<'a> MimeFactory<'a> { if chat.is_self_talk() { recipients.push((from_displayname.to_string(), from_addr.to_string())); } else { - context + let mut rows = context .sql - .query_map( - "SELECT c.authname, c.addr \ + .fetch( + sqlx::query( + "SELECT c.authname, c.addr \ FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ WHERE cc.chat_id=? AND cc.contact_id>9;", - paramsv![msg.chat_id], - |row| { - let authname: String = row.get(0)?; - let addr: String = row.get(1)?; - Ok((authname, addr)) - }, - |rows| { - for row in rows { - let (authname, addr) = row?; - if !recipients_contain_addr(&recipients, &addr) { - recipients.push((authname, addr)); - } - } - Ok(()) - }, + ) + .bind(msg.chat_id), ) .await?; + while let Some(row) = rows.next().await { + let row = row?; + let authname: String = row.try_get(0)?; + let addr: String = row.try_get(1)?; + if !recipients_contain_addr(&recipients, &addr) { + recipients.push((authname, addr)); + } + } - if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await { + if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? { req_mdn = true; } } - let (in_reply_to, references) = context + let row = context .sql - .query_row( - "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", - paramsv![msg.id], - |row| { - let in_reply_to: String = row.get(0)?; - let references: String = row.get(1)?; - - Ok(( - render_rfc724_mid_list(&in_reply_to), - render_rfc724_mid_list(&references), - )) - }, + .fetch_one( + sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?") + .bind(msg.id), ) - .await - .context("Can't get mime_in_reply_to, mime_references")?; + .await?; + let (in_reply_to, references) = ( + render_rfc724_mid_list(row.try_get(0)?), + render_rfc724_mid_list(row.try_get(1)?), + ); let default_str = stock_str::status_line(context).await; let factory = MimeFactory { @@ -166,7 +159,7 @@ impl<'a> MimeFactory<'a> { sender_displayname, selfstatus: context .get_config(Config::Selfstatus) - .await + .await? .unwrap_or(default_str), recipients, timestamp: msg.timestamp_sort, @@ -191,16 +184,16 @@ impl<'a> MimeFactory<'a> { let contact = Contact::load_from_db(context, msg.from_id).await?; let from_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .unwrap_or_default(); let from_displayname = context .get_config(Config::Displayname) - .await + .await? .unwrap_or_default(); let default_str = stock_str::status_line(context).await; let selfstatus = context .get_config(Config::Selfstatus) - .await + .await? .unwrap_or(default_str); let timestamp = dc_create_smeared_timestamp(context).await; @@ -232,7 +225,7 @@ impl<'a> MimeFactory<'a> { ) -> Result, &str)>, Error> { let self_addr = context .get_config(Config::ConfiguredAddr) - .await + .await? .ok_or_else(|| format_err!("Not configured"))?; let mut res = Vec::new(); @@ -309,18 +302,18 @@ impl<'a> MimeFactory<'a> { } } - async fn should_do_gossip(&self, context: &Context) -> bool { + async fn should_do_gossip(&self, context: &Context) -> Result { match &self.loaded { Loaded::Message { chat } => { // beside key- and member-changes, force re-gossip every 48 hours - let gossiped_timestamp = chat.get_gossiped_timestamp(context).await; + let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?; if time() > gossiped_timestamp + (2 * 24 * 60 * 60) { - return true; + Ok(true) + } else { + Ok(self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup) } - - self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup } - Loaded::MDN { .. } => false, + Loaded::MDN { .. } => Ok(false), } } @@ -357,7 +350,7 @@ impl<'a> MimeFactory<'a> { async fn subject_str(&self, context: &Context) -> anyhow::Result { let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject); - Ok(match self.loaded { + let subject = match self.loaded { Loaded::Message { ref chat } => { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { return Ok(stock_str::ac_setup_msg_subject(context).await); @@ -387,16 +380,18 @@ impl<'a> MimeFactory<'a> { if let Some(last_subject) = parent_subject { format!("Re: {}", remove_subject_prefix(last_subject)) } else { - let self_name = match context.get_config(Config::Displayname).await { + let self_name = match context.get_config(Config::Displayname).await? { Some(name) => name, - None => context.get_config(Config::Addr).await.unwrap_or_default(), + None => context.get_config(Config::Addr).await?.unwrap_or_default(), }; stock_str::subject_for_new_contact(context, self_name).await } } Loaded::MDN { .. } => stock_str::read_rcpt(context).await, - }) + }; + + Ok(subject) } pub fn recipients(&self) -> Vec { @@ -567,7 +562,7 @@ impl<'a> MimeFactory<'a> { let outer_message = if is_encrypted { // Add gossip headers in chats with multiple recipients - if peerstates.len() > 1 && self.should_do_gossip(context).await { + if peerstates.len() > 1 && self.should_do_gossip(context).await? { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { if peerstate.peek_key(min_verified).is_some() { if let Some(header) = peerstate.render_gossip_header(min_verified) { @@ -966,7 +961,9 @@ impl<'a> MimeFactory<'a> { // for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message. if self.msg.has_html() { let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) { - MsgId::new(orig_msg_id.try_into()?).get_html(context).await + MsgId::new(orig_msg_id.try_into()?) + .get_html(context) + .await? } else { self.msg.param.get(Param::SendHtml).map(|s| s.to_string()) }; @@ -1009,7 +1006,7 @@ impl<'a> MimeFactory<'a> { } if self.attach_selfavatar { - match context.get_config(Config::Selfavatar).await { + match context.get_config(Config::Selfavatar).await? { Some(path) => match build_selfavatar_file(context, &path) { Ok((part, filename)) => { parts.push(part); @@ -1137,14 +1134,14 @@ async fn build_body_file( // etc. let filename_to_send: String = match msg.viewtype { Viewtype::Voice => chrono::Utc - .timestamp(msg.timestamp_sort as i64, 0) + .timestamp(msg.timestamp_sort, 0) .format(&format!("voice-message_%Y-%m-%d_%H-%M-%S.{}", &suffix)) .to_string(), Viewtype::Image | Viewtype::Gif => format!( "{}.{}", if base_name.is_empty() { chrono::Utc - .timestamp(msg.timestamp_sort as i64, 0) + .timestamp(msg.timestamp_sort, 0) .format("image_%Y-%m-%d_%H-%M-%S") .to_string() } else { @@ -1155,7 +1152,7 @@ async fn build_body_file( Viewtype::Video => format!( "video_{}.{}", chrono::Utc - .timestamp(msg.timestamp_sort as i64, 0) + .timestamp(msg.timestamp_sort, 0) .format("%Y-%m-%d_%H-%M-%S") .to_string(), &suffix diff --git a/src/mimeparser.rs b/src/mimeparser.rs index ca8d77725..47df2b993 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -4,7 +4,6 @@ use std::pin::Pin; use anyhow::{bail, Result}; use charset::Charset; -use deltachat_derive::{FromSql, ToSql}; use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; @@ -103,10 +102,8 @@ pub(crate) enum MailinglistType { None, } -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, -)] -#[repr(i32)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[repr(u32)] pub enum SystemMessage { Unknown = 0, GroupNameChanged = 2, @@ -1215,10 +1212,16 @@ impl MimeMessage { for original_message_id in std::iter::once(&report.original_message_id).chain(&report.additional_message_ids) { - if let Some((chat_id, msg_id)) = - message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await + match message::handle_mdn(context, from_id, original_message_id, sent_timestamp) + .await { - context.emit_event(EventType::MsgRead { chat_id, msg_id }); + Ok(Some((chat_id, msg_id))) => { + context.emit_event(EventType::MsgRead { chat_id, msg_id }); + } + Ok(None) => {} + Err(err) => { + warn!(context, "failed to handle_mdn: {:#}", err); + } } } } @@ -1245,9 +1248,8 @@ impl MimeMessage { { context .sql - .query_get_value_result( - "SELECT timestamp FROM msgs WHERE rfc724_mid=?", - paramsv![field], + .query_get_value( + sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field), ) .await? } else { @@ -1918,8 +1920,9 @@ mod tests { .ctx .sql .execute( - "INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)", - paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp], + sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)") + .bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org") + .bind(timestamp), ) .await .expect("Failed to write to the database"); diff --git a/src/oauth2.rs b/src/oauth2.rs index 370675eea..aec5a8e9e 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use anyhow::Result; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; @@ -58,11 +59,7 @@ pub async fn dc_get_oauth2_url( if let Some(oauth2) = Oauth2::from_address(addr).await { if context .sql - .set_raw_config( - context, - "oauth2_pending_redirect_uri", - Some(redirect_uri.as_ref()), - ) + .set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri.as_ref())) .await .is_err() { @@ -82,31 +79,25 @@ pub async fn dc_get_oauth2_access_token( addr: impl AsRef, code: impl AsRef, regenerate: bool, -) -> Option { +) -> Result> { if let Some(oauth2) = Oauth2::from_address(addr).await { let lock = context.oauth2_mutex.lock().await; // read generated token - if !regenerate && !is_expired(context).await { - let access_token = context - .sql - .get_raw_config(context, "oauth2_access_token") - .await; + if !regenerate && !is_expired(context).await? { + let access_token = context.sql.get_raw_config("oauth2_access_token").await?; if access_token.is_some() { // success - return access_token; + return Ok(access_token); } } // generate new token: build & call auth url - let refresh_token = context - .sql - .get_raw_config(context, "oauth2_refresh_token") - .await; + let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await?; let refresh_token_for = context .sql - .get_raw_config(context, "oauth2_refresh_token_for") - .await + .get_raw_config("oauth2_refresh_token_for") + .await? .unwrap_or_else(|| "unset".into()); let (redirect_uri, token_url, update_redirect_uri_on_success) = @@ -115,8 +106,8 @@ pub async fn dc_get_oauth2_access_token( ( context .sql - .get_raw_config(context, "oauth2_pending_redirect_uri") - .await + .get_raw_config("oauth2_pending_redirect_uri") + .await? .unwrap_or_else(|| "unset".into()), oauth2.init_token, true, @@ -129,8 +120,8 @@ pub async fn dc_get_oauth2_access_token( ( context .sql - .get_raw_config(context, "oauth2_redirect_uri") - .await + .get_raw_config("oauth2_redirect_uri") + .await? .unwrap_or_else(|| "unset".into()), oauth2.refresh_token, false, @@ -166,7 +157,7 @@ pub async fn dc_get_oauth2_access_token( let mut req = surf::post(post_url).build(); if let Err(err) = req.body_form(&post_param) { warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err); - return None; + return Ok(None); } let client = surf::Client::new(); @@ -176,7 +167,7 @@ pub async fn dc_get_oauth2_access_token( context, "Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed ); - return None; + return Ok(None); } // update refresh_token if given, typically on the first round, but we update it later as well. @@ -184,14 +175,12 @@ pub async fn dc_get_oauth2_access_token( if let Some(ref token) = response.refresh_token { context .sql - .set_raw_config(context, "oauth2_refresh_token", Some(token)) - .await - .ok(); + .set_raw_config("oauth2_refresh_token", Some(token)) + .await?; context .sql - .set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref())) - .await - .ok(); + .set_raw_config("oauth2_refresh_token_for", Some(code.as_ref())) + .await?; } // after that, save the access token. @@ -199,9 +188,8 @@ pub async fn dc_get_oauth2_access_token( if let Some(ref token) = response.access_token { context .sql - .set_raw_config(context, "oauth2_access_token", Some(token)) - .await - .ok(); + .set_raw_config("oauth2_access_token", Some(token)) + .await?; let expires_in = response .expires_in // refresh a bit before @@ -209,16 +197,14 @@ pub async fn dc_get_oauth2_access_token( .unwrap_or_else(|| 0); context .sql - .set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in) - .await - .ok(); + .set_raw_config_int64("oauth2_timestamp_expires", expires_in) + .await?; if update_redirect_uri_on_success { context .sql - .set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref())) - .await - .ok(); + .set_raw_config("oauth2_redirect_uri", Some(redirect_uri.as_ref())) + .await?; } } else { warn!(context, "Failed to find OAuth2 access token"); @@ -226,11 +212,11 @@ pub async fn dc_get_oauth2_access_token( drop(lock); - response.access_token + Ok(response.access_token) } else { warn!(context, "Internal OAuth2 error: 2"); - None + Ok(None) } } @@ -238,27 +224,33 @@ pub async fn dc_get_oauth2_addr( context: &Context, addr: impl AsRef, code: impl AsRef, -) -> Option { - let oauth2 = Oauth2::from_address(addr.as_ref()).await?; - oauth2.get_userinfo?; +) -> Result> { + let oauth2 = match Oauth2::from_address(addr.as_ref()).await { + Some(o) => o, + None => return Ok(None), + }; + if oauth2.get_userinfo.is_none() { + return Ok(None); + } if let Some(access_token) = - dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await + dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await? { let addr_out = oauth2.get_addr(context, access_token).await; if addr_out.is_none() { // regenerate - if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await + if let Some(access_token) = + dc_get_oauth2_access_token(context, addr, code, true).await? { - oauth2.get_addr(context, access_token).await + Ok(oauth2.get_addr(context, access_token).await) } else { - None + Ok(None) } } else { - addr_out + Ok(addr_out) } } else { - None + Ok(None) } } @@ -317,21 +309,21 @@ impl Oauth2 { } } -async fn is_expired(context: &Context) -> bool { +async fn is_expired(context: &Context) -> Result { let expire_timestamp = context .sql - .get_raw_config_int64(context, "oauth2_timestamp_expires") - .await + .get_raw_config_int64("oauth2_timestamp_expires") + .await? .unwrap_or_default(); if expire_timestamp <= 0 { - return false; + return Ok(false); } if expire_timestamp > time() { - return false; + return Ok(false); } - true + Ok(true) } fn replace_in_uri(uri: impl AsRef, key: impl AsRef, value: impl AsRef) -> String { @@ -399,7 +391,7 @@ mod tests { let ctx = TestContext::new().await; let addr = "dignifiedquire@gmail.com"; let code = "fail"; - let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await; + let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap(); // this should fail as it is an invalid password assert_eq!(res, None); } @@ -419,7 +411,9 @@ mod tests { let ctx = TestContext::new().await; let addr = "dignifiedquire@gmail.com"; let code = "fail"; - let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await; + let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false) + .await + .unwrap(); // this should fail as it is an invalid password assert_eq!(res, None); } diff --git a/src/param.rs b/src/param.rs index eec7fad89..025bc904e 100644 --- a/src/param.rs +++ b/src/param.rs @@ -333,7 +333,7 @@ impl Params { pub fn get_msg_id(&self) -> Option { self.get(Param::MsgId) - .and_then(|x| x.parse::().ok()) + .and_then(|x| x.parse().ok()) .map(MsgId::new) } diff --git a/src/peerstate.rs b/src/peerstate.rs index efa3a9d33..4b6ff0133 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -5,6 +5,7 @@ use std::fmt; use anyhow::{bail, Result}; use num_traits::FromPrimitive; +use sqlx::Row; use crate::aheader::{Aheader, EncryptPreference}; use crate::chat; @@ -139,12 +140,15 @@ impl Peerstate { } pub async fn from_addr(context: &Context, addr: &str) -> Result> { - let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ + let query = sqlx::query( + "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ verified_key, verified_key_fingerprint \ FROM acpeerstates \ - WHERE addr=? COLLATE NOCASE;"; - Self::from_stmt(context, query, paramsv![addr]).await + WHERE addr=? COLLATE NOCASE;", + ) + .bind(addr); + Self::from_stmt(context, query).await } pub async fn from_fingerprint( @@ -152,72 +156,75 @@ impl Peerstate { _sql: &Sql, fingerprint: &Fingerprint, ) -> Result> { - let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ + let fp = fingerprint.hex(); + let query = sqlx::query( + "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ verified_key, verified_key_fingerprint \ FROM acpeerstates \ WHERE public_key_fingerprint=? COLLATE NOCASE \ OR gossip_key_fingerprint=? COLLATE NOCASE \ - ORDER BY public_key_fingerprint=? DESC;"; - let fp = fingerprint.hex(); - Self::from_stmt(context, query, paramsv![fp, fp, fp]).await + ORDER BY public_key_fingerprint=? DESC;", + ) + .bind(&fp) + .bind(&fp) + .bind(&fp); + + Self::from_stmt(context, query).await } - async fn from_stmt( - context: &Context, - query: &str, - params: Vec<&dyn crate::ToSql>, - ) -> Result> { - let peerstate = context - .sql - .query_row_optional(query, params, |row| { - /* all the above queries start with this: SELECT - addr, last_seen, last_seen_autocrypt, prefer_encrypted, - public_key, gossip_timestamp, gossip_key, public_key_fingerprint, - gossip_key_fingerprint, verified_key, verified_key_fingerprint - */ + async fn from_stmt<'e, 'q, E>(context: &Context, query: E) -> Result> + where + 'q: 'e, + E: 'q + sqlx::Execute<'q, sqlx::Sqlite>, + { + if let Some(row) = context.sql.fetch_optional(query).await? { + // all the above queries start with this: SELECT + // addr, last_seen, last_seen_autocrypt, prefer_encrypted, + // public_key, gossip_timestamp, gossip_key, public_key_fingerprint, + // gossip_key_fingerprint, verified_key, verified_key_fingerprint - let res = Peerstate { - addr: row.get(0)?, - last_seen: row.get(1)?, - last_seen_autocrypt: row.get(2)?, - prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(), - public_key: row - .get(4) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - public_key_fingerprint: row - .get::<_, Option>(7)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_key: row - .get(6) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - gossip_key_fingerprint: row - .get::<_, Option>(8)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_timestamp: row.get(5)?, - verified_key: row - .get(9) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - verified_key_fingerprint: row - .get::<_, Option>(10)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - to_save: None, - fingerprint_changed: false, - }; + let peerstate = Peerstate { + addr: row.try_get(0)?, + last_seen: row.try_get(1)?, + last_seen_autocrypt: row.try_get(2)?, + prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(), + public_key: row + .try_get::<&[u8], _>(4) + .ok() + .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), + public_key_fingerprint: row + .try_get::, _>(7)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + gossip_key: row + .try_get::<&[u8], _>(6) + .ok() + .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), + gossip_key_fingerprint: row + .try_get::, _>(8)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + gossip_timestamp: row.try_get(5)?, + verified_key: row + .try_get::<&[u8], _>(9) + .ok() + .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), + verified_key_fingerprint: row + .try_get::, _>(10)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + to_save: None, + fingerprint_changed: false, + }; - Ok(res) - }) - .await?; - Ok(peerstate) + Ok(Some(peerstate)) + } else { + Ok(None) + } } pub fn recalc_fingerprint(&mut self) { @@ -266,9 +273,8 @@ impl Peerstate { if self.fingerprint_changed { if let Some(contact_id) = context .sql - .query_get_value_result( - "SELECT id FROM contacts WHERE addr=?;", - paramsv![self.addr], + .query_get_value( + sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr), ) .await? { @@ -429,42 +435,59 @@ impl Peerstate { pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> { if self.to_save == Some(ToSave::All) || create { sql.execute( - if create { - "INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \ - public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint, addr \ - ) VALUES(?,?,?,?,?,?,?,?,?,?,?)" + (if create { + sqlx::query( + "INSERT INTO acpeerstates ( \ + last_seen, \ + last_seen_autocrypt, \ + prefer_encrypted, \ + public_key, \ + gossip_timestamp, \ + gossip_key, \ + public_key_fingerprint, \ + gossip_key_fingerprint, \ + verified_key, \ + verified_key_fingerprint, \ + addr \ + ) VALUES(?,?,?,?,?,?,?,?,?,?,?)", + ) } else { - "UPDATE acpeerstates \ - SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \ - public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \ - verified_key=?, verified_key_fingerprint=? \ - WHERE addr=?" - }, - paramsv![ - self.last_seen, - self.last_seen_autocrypt, - self.prefer_encrypt as i64, - self.public_key.as_ref().map(|k| k.to_bytes()), - self.gossip_timestamp, - self.gossip_key.as_ref().map(|k| k.to_bytes()), - self.public_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.verified_key.as_ref().map(|k| k.to_bytes()), - self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.addr, - ], - ).await?; + sqlx::query( + "UPDATE acpeerstates \ + SET last_seen=?, \ + last_seen_autocrypt=?, \ + prefer_encrypted=?, \ + public_key=?, \ + gossip_timestamp=?, \ + gossip_key=?, \ + public_key_fingerprint=?, \ + gossip_key_fingerprint=?, \ + verified_key=?, \ + verified_key_fingerprint=? \ + WHERE addr=?", + ) + }) + .bind(self.last_seen) + .bind(self.last_seen_autocrypt) + .bind(self.prefer_encrypt as i64) + .bind(self.public_key.as_ref().map(|k| k.to_bytes())) + .bind(self.gossip_timestamp) + .bind(self.gossip_key.as_ref().map(|k| k.to_bytes())) + .bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex())) + .bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex())) + .bind(self.verified_key.as_ref().map(|k| k.to_bytes())) + .bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex())) + .bind(&self.addr), + ) + .await?; } else if self.to_save == Some(ToSave::Timestamps) { sql.execute( - "UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ - WHERE addr=?;", - paramsv![ - self.last_seen, - self.last_seen_autocrypt, - self.gossip_timestamp, - self.addr - ], + sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ + WHERE addr=?;").bind( + self.last_seen).bind( + self.last_seen_autocrypt).bind( + self.gossip_timestamp).bind( + &self.addr) ) .await?; } @@ -481,12 +504,6 @@ impl Peerstate { } } -impl From for rusqlite::Error { - fn from(_source: crate::key::FingerprintError) -> Self { - Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text) - } -} - #[cfg(test)] mod tests { use super::*; @@ -619,7 +636,7 @@ mod tests { // can be loaded without errors. ctx.ctx .sql - .execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr]) + .execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr)) .await .expect("Failed to write to the database"); diff --git a/src/qr.rs b/src/qr.rs index 677e00214..8bb6a5f8d 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -791,20 +791,39 @@ mod tests { async fn test_set_config_from_qr() { let ctx = TestContext::new().await; - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none()); + assert!(ctx + .ctx + .get_config(Config::WebrtcInstance) + .await + .unwrap() + .is_none()); let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await; assert!(!res.is_ok()); - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none()); + assert!(ctx + .ctx + .get_config(Config::WebrtcInstance) + .await + .unwrap() + .is_none()); let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await; assert!(!res.is_ok()); - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none()); + assert!(ctx + .ctx + .get_config(Config::WebrtcInstance) + .await + .unwrap() + .is_none()); let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await; assert!(res.is_ok()); assert_eq!( - ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(), + ctx.ctx + .get_config(Config::WebrtcInstance) + .await + .unwrap() + .unwrap(), "https://example.org/" ); @@ -812,7 +831,11 @@ mod tests { set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await; assert!(res.is_ok()); assert_eq!( - ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(), + ctx.ctx + .get_config(Config::WebrtcInstance) + .await + .unwrap() + .unwrap(), "basicwebrtc:https://foo.bar/?$ROOM&test" ); } diff --git a/src/scheduler.rs b/src/scheduler.rs index 46fe6fe5a..a14f8ed71 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -77,7 +77,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne Some(job) => { // Let the fetch run, but return back to the job afterwards. jobs_loaded = 0; - if ctx.get_config_bool(Config::InboxWatch).await { + if ctx + .get_config_bool(Config::InboxWatch) + .await + .unwrap_or_default() + { info!(ctx, "postponing imap-job {} to run fetch...", job); fetch(&ctx, &mut connection).await; } @@ -93,7 +97,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne maybe_add_time_based_warnings(&ctx).await; - info = if ctx.get_config_bool(Config::InboxWatch).await { + info = if ctx + .get_config_bool(Config::InboxWatch) + .await + .unwrap_or_default() + { fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await } else { if let Err(err) = connection.scan_folders(&ctx).await { @@ -121,7 +129,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne async fn fetch(ctx: &Context, connection: &mut Imap) { match ctx.get_config(Config::ConfiguredInboxFolder).await { - Some(watch_folder) => { + Ok(Some(watch_folder)) => { if let Err(err) = connection.connect_configured(ctx).await { error_network!(ctx, "{}", err); return; @@ -133,16 +141,23 @@ async fn fetch(ctx: &Context, connection: &mut Imap) { warn!(ctx, "{:#}", err); } } - None => { + Ok(None) => { warn!(ctx, "Can not fetch inbox folder, not set"); connection.fake_idle(ctx, None).await; } + Err(err) => { + warn!( + ctx, + "Can not fetch inbox folder, failed to get config: {:?}", err + ); + connection.fake_idle(ctx, None).await; + } } } async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo { match ctx.get_config(folder).await { - Some(watch_folder) => { + Ok(Some(watch_folder)) => { // connect and fake idle if unable to connect if let Err(err) = connection.connect_configured(ctx).await { warn!(ctx, "imap connection failed: {}", err); @@ -178,10 +193,17 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int connection.fake_idle(ctx, Some(watch_folder)).await } } - None => { + Ok(None) => { warn!(ctx, "Can not watch {} folder, not set", folder); connection.fake_idle(ctx, None).await } + Err(err) => { + warn!( + ctx, + "Can not watch {} folder, failed to retrieve config: {:?}", folder, err + ); + connection.fake_idle(ctx, None).await + } } } @@ -299,7 +321,11 @@ impl Scheduler { })) }; - if ctx.get_config_bool(Config::MvboxWatch).await { + if ctx + .get_config_bool(Config::MvboxWatch) + .await + .unwrap_or_default() + { let ctx = ctx.clone(); mvbox_handle = Some(task::spawn(async move { simple_imap_loop( @@ -317,7 +343,11 @@ impl Scheduler { .expect("mvbox start send, missing receiver"); } - if ctx.get_config_bool(Config::SentboxWatch).await { + if ctx + .get_config_bool(Config::SentboxWatch) + .await + .unwrap_or_default() + { let ctx = ctx.clone(); sentbox_handle = Some(task::spawn(async move { simple_imap_loop( diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index 49be4f53b..2388b9c6a 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -191,7 +191,7 @@ impl BobState { let chat_id = chat::create_by_contact_id(context, invite.contact_id()) .await .map_err(JoinError::UnknownContact)?; - if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await { + if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? { // The scanned fingerprint matches Alice's key, we can proceed to step 4b. info!(context, "Taking securejoin protocol shortcut"); let state = Self { @@ -300,7 +300,7 @@ impl BobState { self.next = SecureJoinStep::Terminated; return Ok(Some(BobHandshakeStage::Terminated(reason))); } - if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await { + if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? { self.next = SecureJoinStep::Terminated; return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); } diff --git a/src/securejoin/mod.rs b/src/securejoin/mod.rs index 0a8a1aed7..2690704af 100644 --- a/src/securejoin/mod.rs +++ b/src/securejoin/mod.rs @@ -173,9 +173,16 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option) -> O let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await; let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await; let self_addr = match context.get_config(Config::ConfiguredAddr).await { - Some(addr) => addr, - None => { - error!(context, "Not configured, cannot generate QR code.",); + Ok(Some(addr)) => addr, + Ok(None) => { + error!(context, "Not configured, cannot generate QR code."); + return None; + } + Err(err) => { + error!( + context, + "Unable to retrieve configuration, cannot generate QR code: {:?}", err + ); return None; } }; @@ -183,6 +190,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option) -> O let self_name = context .get_config(Config::Displayname) .await + .ok()? .unwrap_or_default(); let fingerprint: Fingerprint = match get_self_fingerprint(context).await { @@ -263,6 +271,8 @@ pub enum JoinError { MissingChat(#[source] sql::Error), #[error("Ongoing sender dropped (this is a bug)")] OngoingSenderDropped, + #[error("Other")] + Other(#[from] anyhow::Error), } /// Take a scanned QR-code and do the setup-contact/join-group/invite handshake. @@ -290,6 +300,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result { info!(context, "Requesting secure-join ...",); let qr_scan = check_qr(context, &qr).await; + let invite = QrInvite::try_from(qr_scan)?; match context.bob.start_protocol(context, invite.clone()).await? { @@ -390,11 +401,11 @@ async fn send_handshake_msg( Ok(()) } -async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 { - if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] { - contact_id +async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result { + if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] { + Ok(contact_id) } else { - 0 + Ok(0) } } @@ -402,8 +413,8 @@ async fn fingerprint_equals_sender( context: &Context, fingerprint: &Fingerprint, contact_chat_id: ChatId, -) -> bool { - if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] { +) -> Result { + if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] { if let Ok(contact) = Contact::load_from_db(context, contact_id).await { let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await { Ok(peerstate) => peerstate, @@ -414,7 +425,7 @@ async fn fingerprint_equals_sender( contact.get_addr(), err ); - return false; + return Ok(false); } }; @@ -422,12 +433,12 @@ async fn fingerprint_equals_sender( if peerstate.public_key_fingerprint.is_some() && fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap() { - return true; + return Ok(true); } } } } - false + Ok(false) } /// What to do with a Secure-Join handshake message after it was handled. @@ -552,7 +563,7 @@ pub(crate) async fn handle_securejoin_handshake( Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { could_not_establish_secure_connection(context, bobstate.chat_id(), why) - .await; + .await?; Ok(HandshakeMessage::Done) } Some(_stage) => { @@ -581,7 +592,7 @@ pub(crate) async fn handle_securejoin_handshake( contact_chat_id, "Fingerprint not provided.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } }; @@ -591,16 +602,16 @@ pub(crate) async fn handle_securejoin_handshake( contact_chat_id, "Auth not encrypted.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } - if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await { + if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? { could_not_establish_secure_connection( context, contact_chat_id, "Fingerprint mismatch on inviter-side.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } info!(context, "Fingerprint verified.",); @@ -613,13 +624,13 @@ pub(crate) async fn handle_securejoin_handshake( contact_chat_id, "Auth not provided.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } }; if !token::exists(context, token::Namespace::Auth, auth_0).await { could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.") - .await; + .await?; return Ok(HandshakeMessage::Ignore); } if mark_peer_as_verified(context, &fingerprint).await.is_err() { @@ -628,12 +639,12 @@ pub(crate) async fn handle_securejoin_handshake( contact_chat_id, "Fingerprint mismatch on inviter-side.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await; info!(context, "Auth verified.",); - secure_connection_established(context, contact_chat_id).await; + secure_connection_established(context, contact_chat_id).await?; emit_event!(context, EventType::ContactsChanged(Some(contact_id))); inviter_progress!(context, contact_id, 600); if join_vg { @@ -693,12 +704,12 @@ pub(crate) async fn handle_securejoin_handshake( Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { could_not_establish_secure_connection(context, bobstate.chat_id(), why) - .await; + .await?; Ok(HandshakeMessage::Done) } Some(BobHandshakeStage::Completed) => { // Can only be BobHandshakeStage::Completed - secure_connection_established(context, bobstate.chat_id()).await; + secure_connection_established(context, bobstate.chat_id()).await?; Ok(retval) } Some(_) => { @@ -812,7 +823,7 @@ pub(crate) async fn observe_securejoin_on_other_device( contact_chat_id, "Message not encrypted correctly.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) @@ -824,7 +835,7 @@ pub(crate) async fn observe_securejoin_on_other_device( contact_chat_id, "Fingerprint not provided, please update Delta Chat on all your devices.", ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } }; @@ -834,7 +845,7 @@ pub(crate) async fn observe_securejoin_on_other_device( contact_chat_id, format!("Fingerprint mismatch on observing {}.", step).as_ref(), ) - .await; + .await?; return Ok(HandshakeMessage::Ignore); } Ok(if step.as_str() == "vg-member-added" { @@ -847,8 +858,11 @@ pub(crate) async fn observe_securejoin_on_other_device( } } -async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) { - let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await; +async fn secure_connection_established( + context: &Context, + contact_chat_id: ChatId, +) -> Result<(), Error> { + let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?; let contact = Contact::get_by_id(context, contact_id).await; let addr = if let Ok(ref contact) = contact { @@ -860,14 +874,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, EventType::ChatModified(contact_chat_id)); info!(context, "StockMessage::ContactVerified posted to 1:1 chat"); + + Ok(()) } async fn could_not_establish_secure_connection( context: &Context, contact_chat_id: ChatId, details: &str, -) { - let contact_id = chat_id_2_contact_id(context, contact_chat_id).await; +) -> Result<(), Error> { + let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?; let contact = Contact::get_by_id(context, contact_id).await; let msg = stock_str::contact_not_verified( context, @@ -884,6 +900,8 @@ async fn could_not_establish_secure_connection( context, "StockMessage::ContactNotVerified posted to 1:1 chat ({})", details ); + + Ok(()) } async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> { @@ -1061,6 +1079,7 @@ mod tests { let chat = alice.create_chat(&bob).await; let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None) .await + .unwrap() .into_iter() .filter_map(|item| match item { chat::ChatItem::Message { msg_id } => Some(msg_id), @@ -1109,6 +1128,7 @@ mod tests { let chat = bob.create_chat(&alice).await; let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None) .await + .unwrap() .into_iter() .filter_map(|item| match item { chat::ChatItem::Message { msg_id } => Some(msg_id), diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index 1fc8e71b0..774ce4a7c 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -22,25 +22,24 @@ const SMTP_TIMEOUT: u64 = 30; pub enum Error { #[error("Bad parameters")] BadParameters, - #[error("Invalid login address {address}: {error}")] InvalidLoginAddress { address: String, #[source] error: error::Error, }, - #[error("SMTP: failed to connect: {0}")] ConnectionFailure(#[source] smtp::error::Error), - #[error("SMTP: failed to setup connection {0:?}")] ConnectionSetupFailure(#[source] smtp::error::Error), - #[error("SMTP: oauth2 error {address}")] Oauth2Error { address: String }, - - #[error("TLS error")] + #[error("TLS error {0}")] Tls(#[from] async_native_tls::Error), + #[error("Sql {0}")] + Sql(#[from] crate::sql::Error), + #[error("{0}")] + Other(#[from] anyhow::Error), } pub type Result = std::result::Result; @@ -100,7 +99,7 @@ impl Smtp { return Ok(()); } - let lp = LoginParam::from_database(context, "configured_").await; + let lp = LoginParam::from_database(context, "configured_").await?; let res = self .connect( context, @@ -164,7 +163,7 @@ impl Smtp { let (creds, mechanism) = if oauth2 { // oauth2 let send_pw = &lp.password; - let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await; + let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?; if access_token.is_none() { return Err(Error::Oauth2Error { address: addr.to_string(), diff --git a/src/smtp/send.rs b/src/smtp/send.rs index eaac4f828..3b9acd973 100644 --- a/src/smtp/send.rs +++ b/src/smtp/send.rs @@ -15,12 +15,12 @@ pub type Result = std::result::Result; pub enum Error { #[error("Envelope error: {}", _0)] EnvelopeError(#[from] async_smtp::error::Error), - #[error("Send error: {}", _0)] SendError(#[from] async_smtp::smtp::error::Error), - #[error("SMTP has no transport")] NoTransport, + #[error("{}", _0)] + Other(#[from] anyhow::Error), } impl Smtp { @@ -36,7 +36,7 @@ impl Smtp { let message_len_bytes = message.len(); let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO; - if let Some(provider) = context.get_configured_provider().await { + if let Some(provider) = context.get_configured_provider().await? { if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to { chunk_size = max_smtp_rcpt_to as usize; } diff --git a/src/sql.rs b/src/sql.rs deleted file mode 100644 index e030d6f50..000000000 --- a/src/sql.rs +++ /dev/null @@ -1,1665 +0,0 @@ -//! # SQLite wrapper - -use async_std::prelude::*; -use async_std::sync::RwLock; - -use std::collections::HashSet; -use std::path::Path; -use std::time::Duration; - -use anyhow::format_err; -use anyhow::Context as _; -use rusqlite::{Connection, Error as SqlError, OpenFlags}; - -use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; -use crate::config::Config; -use crate::config::Config::DeleteServerAfter; -use crate::constants::{ShowEmails, Viewtype, DC_CHAT_ID_TRASH}; -use crate::context::Context; -use crate::dc_tools::{dc_delete_file, time, EmailAddress}; -use crate::ephemeral::start_ephemeral_timers; -use crate::imap; -use crate::message::Message; -use crate::param::{Param, Params}; -use crate::peerstate::Peerstate; -use crate::provider::get_provider_by_domain; -use crate::stock_str; - -#[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:?}")] - Sql(#[from] rusqlite::Error), - #[error("Sqlite Connection Pool Error: {0:?}")] - ConnectionPool(#[from] r2d2::Error), - #[error("Sqlite: Connection closed")] - SqlNoConnection, - #[error("Sqlite: Already open")] - SqlAlreadyOpen, - #[error("Sqlite: Failed to open")] - SqlFailedToOpen, - #[error("{0}")] - Io(#[from] std::io::Error), - #[error("{0:?}")] - BlobError(#[from] crate::blob::BlobError), - #[error("{0}")] - Other(#[from] anyhow::Error), -} - -pub type Result = std::result::Result; - -/// A wrapper around the underlying Sqlite3 object. -#[derive(Debug)] -pub struct Sql { - pool: RwLock>>, -} - -impl Default for Sql { - fn default() -> Self { - Self { - pool: RwLock::new(None), - } - } -} - -impl Sql { - pub fn new() -> Sql { - Self::default() - } - - pub async fn is_open(&self) -> bool { - self.pool.read().await.is_some() - } - - pub async fn close(&self) { - let _ = self.pool.write().await.take(); - // drop closes the connection - } - - pub async fn open>( - &self, - context: &Context, - dbfile: T, - readonly: bool, - ) -> anyhow::Result<()> { - let res = open(context, self, &dbfile, readonly).await; - if let Err(err) = &res { - match err.downcast_ref::() { - Some(Error::SqlAlreadyOpen) => {} - _ => { - self.close().await; - } - } - } - res.map_err(|e| { - format_err!( - // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: - "Could not open db file {}: {:#}", - dbfile.as_ref().to_string_lossy(), - e - ) - }) - } - - 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) - }; - - 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 async fn query_map( - &self, - sql: impl AsRef, - params: Vec<&dyn crate::ToSql>, - f: F, - mut g: G, - ) -> Result - where - F: FnMut(&rusqlite::Row) -> rusqlite::Result, - G: FnMut(rusqlite::MappedRows) -> Result, - { - 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(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(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(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 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)?; - stmt.exists(¶ms) - }; - - res.map_err(Into::into) - } - - /// Execute a query which is expected to return one row. - pub async fn query_row( - &self, - sql: impl AsRef, - params: Vec<&dyn crate::ToSql>, - f: F, - ) -> Result - where - F: FnOnce(&rusqlite::Row) -> rusqlite::Result, - { - 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 - } - - /// Check if a column exists in a given table. - pub async fn col_exists( - &self, - table_name: impl AsRef, - col_name: impl AsRef, - ) -> Result { - let table_name = table_name.as_ref().to_string(); - let col_name = col_name.as_ref().to_string(); - self.with_conn(move |conn| { - let mut exists = false; - // `PRAGMA table_info` returns one row per column, - // each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value - conn.pragma(None, "table_info", &table_name, |row| { - let curr_name: String = row.get(1)?; - if col_name == curr_name { - exists = true; - } - Ok(()) - })?; - - Ok(exists) - }) - .await - } - - /// Execute a query which is expected to return zero or one row. - pub async fn query_row_optional( - &self, - sql: impl AsRef, - params: Vec<&dyn crate::ToSql>, - f: F, - ) -> Result> - where - F: FnOnce(&rusqlite::Row) -> rusqlite::Result, - { - 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( - _, - _, - rusqlite::types::Type::Null, - ))) => Ok(None), - Err(err) => Err(err), - } - } - - /// Executes a query which is expected to return one row and one - /// column. If the query does not return a value or returns SQL - /// `NULL`, returns `Ok(None)`. - pub async fn query_get_value_result( - &self, - query: &str, - params: Vec<&dyn crate::ToSql>, - ) -> Result> - where - 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 async fn query_get_value( - &self, - context: &Context, - query: &str, - params: Vec<&dyn crate::ToSql>, - ) -> Option - where - T: rusqlite::types::FromSql, - { - match self.query_get_value_result(query, params).await { - Ok(res) => res, - Err(err) => { - warn!(context, "sql: Failed query_row: {}", err); - None - } - } - } - - /// Set private configuration options. - /// - /// Setting `None` deletes the value. On failure an error message - /// will already have been logged. - pub async fn set_raw_config( - &self, - context: &Context, - key: impl AsRef, - value: Option<&str>, - ) -> Result<()> { - 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(value) = value { - let exists = self - .exists("SELECT value FROM config WHERE keyname=?;", paramsv![key]) - .await?; - if exists { - self.execute( - "UPDATE config SET value=? WHERE keyname=?;", - paramsv![(*value).to_string(), key.to_string()], - ) - .await - } else { - self.execute( - "INSERT INTO config (keyname, value) VALUES (?, ?);", - paramsv![key.to_string(), (*value).to_string()], - ) - .await - } - } else { - self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key]) - .await - }; - - match res { - Ok(_) => Ok(()), - Err(err) => { - error!(context, "set_raw_config(): Cannot change value. {:?}", &err); - Err(err) - } - } - } - - /// Get configuration options from the database. - 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=?;", - paramsv![key.as_ref().to_string()], - ) - .await - } - - 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 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 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. - let res = self.get_raw_config_int(context, key).await; - res.unwrap_or_default() > 0 - } - - 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).await - } - - 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 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()) - } - - /// 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) - }; - - res.map_err(Into::into) - } - - 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) - }; - - res.map_err(Into::into) - } -} - -pub fn get_rowid( - conn: &mut Connection, - table: impl AsRef, - field: impl AsRef, - value: impl AsRef, -) -> 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. - let query = format!( - "SELECT id FROM {} WHERE {}=? ORDER BY id DESC", - table.as_ref(), - field.as_ref(), - ); - - conn.query_row(&query, params![value.as_ref()], |row| row.get::<_, u32>(0)) -} - -pub fn get_rowid2( - conn: &mut Connection, - table: impl AsRef, - field: impl AsRef, - value: i64, - field2: impl AsRef, - value2: i32, -) -> std::result::Result { - conn.query_row( - &format!( - "SELECT id FROM {} WHERE {}={} AND {}={} ORDER BY id DESC", - table.as_ref(), - field.as_ref(), - value, - field2.as_ref(), - value2, - ), - params![], - |row| row.get::<_, u32>(0), - ) -} - -pub async fn housekeeping(context: &Context) -> anyhow::Result<()> { - if let Err(err) = crate::ephemeral::delete_expired_messages(context).await { - warn!(context, "Failed to delete expired messages: {}", err); - } - - let mut files_in_use = HashSet::new(); - let mut unreferenced_count = 0; - - info!(context, "Start housekeeping..."); - maybe_add_from_param( - context, - &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;", - paramsv![], - |row| row.get::<_, String>(0), - |rows| { - for row in rows { - maybe_add_file(&mut files_in_use, row?); - } - Ok(()) - }, - ) - .await - .context("housekeeping: failed to SELECT value FROM config")?; - - info!(context, "{} files in use.", files_in_use.len(),); - /* go through directory and delete unused files */ - let p = context.get_blobdir(); - match async_std::fs::read_dir(p).await { - Ok(mut dir_handle) => { - /* avoid deletion of files that are just created to build a message object */ - let diff = std::time::Duration::from_secs(60 * 60); - let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap(); - - while let Some(entry) = dir_handle.next().await { - if entry.is_err() { - break; - } - let entry = entry.unwrap(); - let name_f = entry.file_name(); - let name_s = name_f.to_string_lossy(); - - if is_file_in_use(&files_in_use, None, &name_s) - || is_file_in_use(&files_in_use, Some(".increation"), &name_s) - || is_file_in_use(&files_in_use, Some(".waveform"), &name_s) - || is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s) - { - continue; - } - - unreferenced_count += 1; - - if let Ok(stats) = async_std::fs::metadata(entry.path()).await { - let recently_created = - stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than; - let recently_modified = stats.modified().is_ok() - && stats.modified().unwrap() > keep_files_newer_than; - let recently_accessed = stats.accessed().is_ok() - && stats.accessed().unwrap() > keep_files_newer_than; - - if recently_created || recently_modified || recently_accessed { - info!( - context, - "Housekeeping: Keeping new unreferenced file #{}: {:?}", - unreferenced_count, - entry.file_name(), - ); - continue; - } - } - info!( - context, - "Housekeeping: Deleting unreferenced file #{}: {:?}", - unreferenced_count, - entry.file_name() - ); - let path = entry.path(); - dc_delete_file(context, path).await; - } - } - Err(err) => { - warn!( - context, - "Housekeeping: Cannot open {}. ({})", - context.get_blobdir().display(), - err - ); - } - } - - if let Err(err) = start_ephemeral_timers(context).await { - warn!( - context, - "Housekeeping: cannot start ephemeral timers: {}", err - ); - } - - if let Err(err) = prune_tombstones(context).await { - warn!( - context, - "Housekeeping: Cannot prune message tombstones: {}", err - ); - } - - if let Err(e) = context - .set_config(Config::LastHousekeeping, Some(&time().to_string())) - .await - { - warn!(context, "Can't set config: {}", e); - } - info!(context, "Housekeeping done."); - Ok(()) -} - -#[allow(clippy::indexing_slicing)] -fn is_file_in_use(files_in_use: &HashSet, namespc_opt: Option<&str>, name: &str) -> bool { - let name_to_check = if let Some(namespc) = namespc_opt { - let name_len = name.len(); - let namespc_len = namespc.len(); - if name_len <= namespc_len || !name.ends_with(namespc) { - return false; - } - &name[..name_len - namespc_len] - } else { - name - }; - files_in_use.contains(name_to_check) -} - -fn maybe_add_file(files_in_use: &mut HashSet, file: impl AsRef) { - if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") { - files_in_use.insert(file.to_string()); - } -} - -async fn maybe_add_from_param( - context: &Context, - files_in_use: &mut HashSet, - query: &str, - param_id: Param, -) -> anyhow::Result<()> { - context - .sql - .query_map( - query, - paramsv![], - |row| row.get::<_, String>(0), - |rows| { - for row in rows { - let param: Params = row?.parse().unwrap_or_default(); - if let Some(file) = param.get(param_id) { - maybe_add_file(files_in_use, file); - } - } - Ok(()) - }, - ) - .await - .context(format!("housekeeping: failed to add_from_param {}", query)) -} - -#[allow(clippy::cognitive_complexity)] -async fn open( - context: &Context, - sql: &Sql, - dbfile: impl AsRef, - readonly: bool, -) -> anyhow::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 = {}; - PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android - ", - 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; - // Init tables to dbversion=68 - let mut dbversion_before_update: i32 = 68; - if !sql.table_exists("config").await? { - info!( - context, - "First time init: creating tables in {:?}.", - dbfile.as_ref(), - ); - sql.with_conn(move |mut conn| { - let tx = conn.transaction()?; - tx.execute_batch( - r#" -CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT); -CREATE INDEX config_index1 ON config (keyname); -CREATE TABLE contacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT DEFAULT '', - addr TEXT DEFAULT '' COLLATE NOCASE, - origin INTEGER DEFAULT 0, - blocked INTEGER DEFAULT 0, - last_seen INTEGER DEFAULT 0, - param TEXT DEFAULT '', - authname TEXT DEFAULT '', - selfavatar_sent INTEGER DEFAULT 0 -); -CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE); -CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE); -INSERT INTO contacts (id,name,origin) VALUES -(1,'self',262144), (2,'info',262144), (3,'rsvd',262144), -(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144), -(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144); - -CREATE TABLE chats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - type INTEGER DEFAULT 0, - name TEXT DEFAULT '', - draft_timestamp INTEGER DEFAULT 0, - draft_txt TEXT DEFAULT '', - blocked INTEGER DEFAULT 0, - grpid TEXT DEFAULT '', - param TEXT DEFAULT '', - archived INTEGER DEFAULT 0, - gossiped_timestamp INTEGER DEFAULT 0, - locations_send_begin INTEGER DEFAULT 0, - locations_send_until INTEGER DEFAULT 0, - locations_last_sent INTEGER DEFAULT 0, - created_timestamp INTEGER DEFAULT 0, - muted_until INTEGER DEFAULT 0, - ephemeral_timer INTEGER -); -CREATE INDEX chats_index1 ON chats (grpid); -CREATE INDEX chats_index2 ON chats (archived); -CREATE INDEX chats_index3 ON chats (locations_send_until); -INSERT INTO chats (id,type,name) VALUES -(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'), -(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'), -(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd'); - -CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER); -CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id); -CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id); - -CREATE TABLE msgs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rfc724_mid TEXT DEFAULT '', - server_folder TEXT DEFAULT '', - server_uid INTEGER DEFAULT 0, - chat_id INTEGER DEFAULT 0, - from_id INTEGER DEFAULT 0, - to_id INTEGER DEFAULT 0, - timestamp INTEGER DEFAULT 0, - type INTEGER DEFAULT 0, - state INTEGER DEFAULT 0, - msgrmsg INTEGER DEFAULT 1, - bytes INTEGER DEFAULT 0, - txt TEXT DEFAULT '', - txt_raw TEXT DEFAULT '', - param TEXT DEFAULT '', - starred INTEGER DEFAULT 0, - timestamp_sent INTEGER DEFAULT 0, - timestamp_rcvd INTEGER DEFAULT 0, - hidden INTEGER DEFAULT 0, - mime_headers TEXT, - mime_in_reply_to TEXT, - mime_references TEXT, - move_state INTEGER DEFAULT 1, - location_id INTEGER DEFAULT 0, - error TEXT DEFAULT '', - --- Timer value in seconds. For incoming messages this --- timer starts when message is read, so we want to have --- the value stored here until the timer starts. - ephemeral_timer INTEGER DEFAULT 0, - --- Timestamp indicating when the message should be --- deleted. It is convenient to store it here because UI --- needs this value to display how much time is left until --- the message is deleted. - ephemeral_timestamp INTEGER DEFAULT 0 -); - -CREATE INDEX msgs_index1 ON msgs (rfc724_mid); -CREATE INDEX msgs_index2 ON msgs (chat_id); -CREATE INDEX msgs_index3 ON msgs (timestamp); -CREATE INDEX msgs_index4 ON msgs (state); -CREATE INDEX msgs_index5 ON msgs (starred); -CREATE INDEX msgs_index6 ON msgs (location_id); -CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id); -INSERT INTO msgs (id,msgrmsg,txt) VALUES -(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'), -(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'), -(8,0,'rsvd'), (9,0,'daymarker'); - -CREATE TABLE jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - added_timestamp INTEGER, - desired_timestamp INTEGER DEFAULT 0, - action INTEGER, - foreign_id INTEGER, - param TEXT DEFAULT '', - thread INTEGER DEFAULT 0, - tries INTEGER DEFAULT 0 -); -CREATE INDEX jobs_index1 ON jobs (desired_timestamp); - -CREATE TABLE leftgrps ( - id INTEGER PRIMARY KEY, - grpid TEXT DEFAULT '' -); -CREATE INDEX leftgrps_index1 ON leftgrps (grpid); - -CREATE TABLE keypairs ( - id INTEGER PRIMARY KEY, - addr TEXT DEFAULT '' COLLATE NOCASE, - is_default INTEGER DEFAULT 0, - private_key, - public_key, - created INTEGER DEFAULT 0 -); - -CREATE TABLE acpeerstates ( - id INTEGER PRIMARY KEY, - addr TEXT DEFAULT '' COLLATE NOCASE, - last_seen INTEGER DEFAULT 0, - last_seen_autocrypt INTEGER DEFAULT 0, - public_key, - prefer_encrypted INTEGER DEFAULT 0, - gossip_timestamp INTEGER DEFAULT 0, - gossip_key, - public_key_fingerprint TEXT DEFAULT '', - gossip_key_fingerprint TEXT DEFAULT '', - verified_key, - verified_key_fingerprint TEXT DEFAULT '' -); -CREATE INDEX acpeerstates_index1 ON acpeerstates (addr); -CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint); -CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint); -CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint); - -CREATE TABLE msgs_mdns ( - msg_id INTEGER, - contact_id INTEGER, - timestamp_sent INTEGER DEFAULT 0 -); -CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id); - -CREATE TABLE tokens ( - id INTEGER PRIMARY KEY, - namespc INTEGER DEFAULT 0, - foreign_id INTEGER DEFAULT 0, - token TEXT DEFAULT '', - timestamp INTEGER DEFAULT 0 -); - -CREATE TABLE locations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - latitude REAL DEFAULT 0.0, - longitude REAL DEFAULT 0.0, - accuracy REAL DEFAULT 0.0, - timestamp INTEGER DEFAULT 0, - chat_id INTEGER DEFAULT 0, - from_id INTEGER DEFAULT 0, - independent INTEGER DEFAULT 0 -); -CREATE INDEX locations_index1 ON locations (from_id); -CREATE INDEX locations_index2 ON locations (timestamp); - -CREATE TABLE devmsglabels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - label TEXT, - msg_id INTEGER DEFAULT 0 -); -CREATE INDEX devmsglabels_index1 ON devmsglabels (label); -"#, - )?; - tx.commit()?; - Ok(()) - }) - .await?; - - sql.set_raw_config_int(context, "dbversion", dbversion_before_update) - .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 = !exists_before_update; - let mut disable_server_delete = 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?; - 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?; - 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?; - 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?; - 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?; - 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?; - // 'starred' column is not used currently - // (dropping is not easily doable and stop adding it will make reusing it complicated) - sql.execute( - "ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;", - paramsv![], - ) - .await?; - sql.execute("CREATE INDEX msgs_index5 ON msgs (starred);", paramsv![]) - .await?; - 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?; - 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?; - 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; - 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?; - 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?; - 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?; - 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?; - 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?; - 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?; - 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?; - } - 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?; - 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?; - 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?; - } - if dbversion < 64 { - info!(context, "[migration] v64"); - sql.execute( - "ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 64).await?; - } - if dbversion < 65 { - info!(context, "[migration] v65"); - sql.execute( - "ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER", - paramsv![], - ) - .await?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0", - paramsv![], - ) - .await?; - sql.execute( - "ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 65).await?; - } - if dbversion < 66 { - info!(context, "[migration] v66"); - update_icons = true; - sql.set_raw_config_int(context, "dbversion", 66).await?; - } - if dbversion < 67 { - info!(context, "[migration] v67"); - for prefix in &["", "configured_"] { - if let Some(server_flags) = sql - .get_raw_config_int(context, format!("{}server_flags", prefix)) - .await - { - let imap_socket_flags = server_flags & 0x700; - let key = format!("{}mail_security", prefix); - match imap_socket_flags { - 0x100 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS - 0x200 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS - 0x400 => sql.set_raw_config_int(context, key, 3).await?, // Plain - _ => sql.set_raw_config_int(context, key, 0).await?, - } - let smtp_socket_flags = server_flags & 0x70000; - let key = format!("{}send_security", prefix); - match smtp_socket_flags { - 0x10000 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS - 0x20000 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS - 0x40000 => sql.set_raw_config_int(context, key, 3).await?, // Plain - _ => sql.set_raw_config_int(context, key, 0).await?, - } - } - } - sql.set_raw_config_int(context, "dbversion", 67).await?; - } - if dbversion < 68 { - info!(context, "[migration] v68"); - // the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat() - sql.execute( - "CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 68).await?; - } - if dbversion < 69 { - info!(context, "[migration] v69"); - sql.execute( - "ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;", - paramsv![], - ) - .await?; - sql.execute( - "UPDATE chats SET protected=1, type=120 WHERE type=130;", // 120=group, 130=old verified group - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 69).await?; - } - if dbversion < 71 { - info!(context, "[migration] v71"); - if let Some(addr) = context.get_config(Config::ConfiguredAddr).await { - if let Ok(domain) = addr.parse::().map(|email| email.domain) { - context - .set_config( - Config::ConfiguredProvider, - get_provider_by_domain(&domain).map(|provider| provider.id), - ) - .await?; - } else { - warn!(context, "Can't parse configured address: {:?}", addr); - } - } - - sql.set_raw_config_int(context, "dbversion", 71).await?; - } - if dbversion < 72 { - info!(context, "[migration] v72"); - if !sql.col_exists("msgs", "mime_modified").await? { - sql.execute( - "ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;", - paramsv![], - ) - .await?; - } - sql.set_raw_config_int(context, "dbversion", 72).await?; - } - if dbversion < 73 { - use Config::*; - info!(context, "[migration] v73"); - sql.execute( - "CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);", - paramsv![], - ) - .await?; - for c in &[ - ConfiguredInboxFolder, - ConfiguredSentboxFolder, - ConfiguredMvboxFolder, - ] { - if let Some(folder) = context.get_config(*c).await { - let (uid_validity, last_seen_uid) = - imap::get_config_last_seen_uid(context, &folder).await; - if last_seen_uid > 0 { - imap::set_uid_next(context, &folder, last_seen_uid + 1).await?; - imap::set_uidvalidity(context, &folder, uid_validity).await?; - } - } - } - if exists_before_update { - disable_server_delete = true; - - // Don't disable server delete if it was on by default (Nauta): - if let Some(provider) = context.get_configured_provider().await { - if let Some(defaults) = &provider.config_defaults { - if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) { - disable_server_delete = false; - } - } - } - } - sql.set_raw_config_int(context, "dbversion", 73).await?; - } - if dbversion < 74 { - info!(context, "[migration] v74"); - sql.execute( - "UPDATE contacts SET name='' WHERE name=authname", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 74).await?; - } - if dbversion < 75 { - info!(context, "[migration] v75"); - sql.execute( - "ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 75).await?; - } - if dbversion < 76 { - info!(context, "[migration] v76"); - sql.execute( - "ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", - paramsv![], - ) - .await?; - sql.set_raw_config_int(context, "dbversion", 76).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?; - } - if disable_server_delete { - // We now always watch all folders and delete messages there if delete_server is enabled. - // So, for people who have delete_server enabled, disable it and add a hint to the devicechat: - if context.get_config_delete_server_after().await.is_some() { - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(stock_str::delete_server_turned_off(context).await); - add_device_msg(context, None, Some(&mut msg)).await?; - context.set_config(DeleteServerAfter, Some("0")).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 async_std::fs::File; - - use super::*; - use crate::{test_utils::TestContext, Event, EventType}; - - #[test] - fn test_maybe_add_file() { - let mut files = Default::default(); - maybe_add_file(&mut files, "$BLOBDIR/hello"); - maybe_add_file(&mut files, "$BLOBDIR/world.txt"); - maybe_add_file(&mut files, "world2.txt"); - maybe_add_file(&mut files, "$BLOBDIR"); - - assert!(files.contains("hello")); - assert!(files.contains("world.txt")); - assert!(!files.contains("world2.txt")); - assert!(!files.contains("$BLOBDIR")); - } - - #[test] - fn test_is_file_in_use() { - let mut files = Default::default(); - maybe_add_file(&mut files, "$BLOBDIR/hello"); - maybe_add_file(&mut files, "$BLOBDIR/world.txt"); - maybe_add_file(&mut files, "world2.txt"); - - assert!(is_file_in_use(&files, None, "hello")); - assert!(!is_file_in_use(&files, Some(".txt"), "hello")); - assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix")); - } - - #[async_std::test] - async fn test_table_exists() { - let t = TestContext::new().await; - assert!(t.ctx.sql.table_exists("msgs").await.unwrap()); - assert!(!t.ctx.sql.table_exists("foobar").await.unwrap()); - } - - #[async_std::test] - async fn test_col_exists() { - let t = TestContext::new().await; - assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap()); - assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap()); - assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap()); - } - - #[async_std::test] - async fn test_housekeeping_db_closed() { - let t = TestContext::new().await; - - let avatar_src = t.dir.path().join("avatar.png"); - let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); - File::create(&avatar_src) - .await - .unwrap() - .write_all(avatar_bytes) - .await - .unwrap(); - t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await - .unwrap(); - - t.add_event_sink(move |event: Event| async move { - match event.typ { - EventType::Info(s) => assert!( - !s.contains("Keeping new unreferenced file"), - "File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)", - s - ), - EventType::Error(s) => panic!(s), - _ => {} - } - }) - .await; - - let a = t.get_config(Config::Selfavatar).await.unwrap(); - assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); - - t.sql.close().await; - housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed - t.sql.open(&t, &t.get_dbfile(), false).await.unwrap(); - - let a = t.get_config(Config::Selfavatar).await.unwrap(); - assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); - } -} diff --git a/src/sql/error.rs b/src/sql/error.rs new file mode 100644 index 000000000..5fa71a36f --- /dev/null +++ b/src/sql/error.rs @@ -0,0 +1,19 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Sqlx: {0:?}")] + Sqlx(#[from] sqlx::Error), + #[error("Sqlite: Connection closed")] + SqlNoConnection, + #[error("Sqlite: Already open")] + SqlAlreadyOpen, + #[error("Sqlite: Failed to open")] + SqlFailedToOpen, + #[error("{0}")] + Io(#[from] std::io::Error), + // #[error("{0:?}")] + // BlobError(#[from] crate::blob::BlobError), + #[error("{0}")] + Other(#[from] anyhow::Error), +} + +pub type Result = std::result::Result; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs new file mode 100644 index 000000000..f7ff2b624 --- /dev/null +++ b/src/sql/migrations.rs @@ -0,0 +1,505 @@ +use async_std::prelude::*; + +use super::{Result, Sql}; +use crate::config::Config; +use crate::constants::ShowEmails; +use crate::context::Context; +use crate::dc_tools::EmailAddress; +use crate::imap; +use crate::provider::get_provider_by_domain; + +const DBVERSION: i32 = 68; +const VERSION_CFG: &str = "dbversion"; +const TABLES: &str = include_str!("./tables.sql"); + +pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> { + let mut recalc_fingerprints = false; + let mut exists_before_update = false; + let mut dbversion_before_update = DBVERSION; + + if !sql.table_exists("config").await? { + info!(context, "First time init: creating tables",); + sql.transaction(move |conn| { + Box::pin(async move { + sqlx::query(TABLES) + .execute_many(&mut *conn) + .await + .collect::, _>>() + .await?; + + // set raw config inside the transaction + sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") + .bind(VERSION_CFG) + .bind(format!("{}", dbversion_before_update)) + .execute(&mut *conn) + .await?; + Ok(()) + }) + }) + .await?; + } else { + exists_before_update = true; + dbversion_before_update = sql + .get_raw_config_int(VERSION_CFG) + .await? + .unwrap_or_default(); + } + + let dbversion = dbversion_before_update; + let mut update_icons = !exists_before_update; + let mut disable_server_delete = false; + + if dbversion < 1 { + info!(context, "[migration] v1"); + sql.execute_migration( + r#" +CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT ''); +CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#, + 1, + ) + .await?; + } + if dbversion < 2 { + info!(context, "[migration] v2"); + sql.execute_migration( + "ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';", + 2, + ) + .await?; + } + if dbversion < 7 { + info!(context, "[migration] v7"); + sql.execute_migration( + "CREATE TABLE keypairs (\ + id INTEGER PRIMARY KEY, \ + addr TEXT DEFAULT '' COLLATE NOCASE, \ + is_default INTEGER DEFAULT 0, \ + private_key, \ + public_key, \ + created INTEGER DEFAULT 0);", + 7, + ) + .await?; + } + if dbversion < 10 { + info!(context, "[migration] v10"); + sql.execute_migration( + "CREATE TABLE acpeerstates (\ + id INTEGER PRIMARY KEY, \ + addr TEXT DEFAULT '' COLLATE NOCASE, \ + last_seen INTEGER DEFAULT 0, \ + last_seen_autocrypt INTEGER DEFAULT 0, \ + public_key, \ + prefer_encrypted INTEGER DEFAULT 0); \ + CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);", + 10, + ) + .await?; + } + if dbversion < 12 { + info!(context, "[migration] v12"); + sql.execute_migration( + r#" +CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER); +CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#, + 12, + ) + .await?; + } + if dbversion < 17 { + info!(context, "[migration] v17"); + sql.execute_migration( + r#" +ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0; +CREATE INDEX chats_index2 ON chats (archived); +-- 'starred' column is not used currently +-- (dropping is not easily doable and stop adding it will make reusing it complicated) +ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0; +CREATE INDEX msgs_index5 ON msgs (starred);"#, + 17, + ) + .await?; + } + if dbversion < 18 { + info!(context, "[migration] v18"); + sql.execute_migration( + r#" +ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0; +ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#, + 18, + ) + .await?; + } + if dbversion < 27 { + info!(context, "[migration] v27"); + // chat.id=1 and chat.id=2 are the old deaddrops, + // the current ones are defined by chats.blocked=2 + sql.execute_migration( + r#" +DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;" +CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);" +ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;") +ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#, + 27, + ) + .await?; + } + if dbversion < 34 { + info!(context, "[migration] v34"); + sql.execute_migration( + r#" +ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0; +ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0; +ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT ''; +ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT ''; +CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint); +CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#, + 34, + ) + .await?; + recalc_fingerprints = true; + } + if dbversion < 39 { + info!(context, "[migration] v39"); + sql.execute_migration( + r#" +CREATE TABLE tokens ( + id INTEGER PRIMARY KEY, + namespc INTEGER DEFAULT 0, + foreign_id INTEGER DEFAULT 0, + token TEXT DEFAULT '', + timestamp INTEGER DEFAULT 0 +); +ALTER TABLE acpeerstates ADD COLUMN verified_key; +ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT ''; +CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#, + 38, + ) + .await?; + } + if dbversion < 40 { + info!(context, "[migration] v40"); + sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40) + .await?; + } + if dbversion < 44 { + info!(context, "[migration] v44"); + sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44) + .await?; + } + if dbversion < 46 { + info!(context, "[migration] v46"); + sql.execute_migration( + r#" +ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT; +ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#, + 46, + ) + .await?; + } + if dbversion < 47 { + info!(context, "[migration] v47"); + sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47) + .await?; + } + if dbversion < 48 { + info!(context, "[migration] v48"); + // NOTE: move_state is not used anymore + sql.execute_migration( + "ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;", + 48, + ) + .await?; + } + if dbversion < 49 { + info!(context, "[migration] v49"); + sql.execute_migration( + "ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;", + 49, + ) + .await?; + } + if dbversion < 50 { + info!(context, "[migration] v50"); + // installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly; + // keep this default and use DC_SHOW_EMAILS_NO + // only for new installations + if exists_before_update { + sql.set_raw_config_int("show_emails", ShowEmails::All as i32) + .await?; + } + sql.set_db_version(50).await?; + } + if dbversion < 53 { + info!(context, "[migration] v53"); + // the messages containing _only_ locations + // are also added to the database as _hidden_. + sql.execute_migration( + r#" +CREATE TABLE locations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + latitude REAL DEFAULT 0.0, + longitude REAL DEFAULT 0.0, + accuracy REAL DEFAULT 0.0, + timestamp INTEGER DEFAULT 0, + chat_id INTEGER DEFAULT 0, + from_id INTEGER DEFAULT 0 +);" +CREATE INDEX locations_index1 ON locations (from_id); +CREATE INDEX locations_index2 ON locations (timestamp); +ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0; +ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0; +ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0; +CREATE INDEX chats_index3 ON chats (locations_send_until);"#, + 53, + ) + .await?; + } + if dbversion < 54 { + info!(context, "[migration] v54"); + sql.execute_migration( + r#" +ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0; +CREATE INDEX msgs_index6 ON msgs (location_id);"#, + 54, + ) + .await?; + } + if dbversion < 55 { + info!(context, "[migration] v55"); + sql.execute_migration( + "ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;", + 55, + ) + .await?; + } + if dbversion < 59 { + info!(context, "[migration] v59"); + // records in the devmsglabels are kept when the message is deleted. + // so, msg_id may or may not exist. + sql.execute_migration( + r#" +CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);", +CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59) + .await?; + if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() { + sql.set_raw_config_int("bcc_self", 1).await?; + } + } + + if dbversion < 60 { + info!(context, "[migration] v60"); + sql.execute_migration( + "ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;", + 60, + ) + .await?; + } + if dbversion < 61 { + info!(context, "[migration] v61"); + sql.execute_migration( + "ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;", + 61, + ) + .await?; + update_icons = true; + } + if dbversion < 62 { + info!(context, "[migration] v62"); + sql.execute_migration( + "ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;", + 62, + ) + .await?; + } + if dbversion < 63 { + info!(context, "[migration] v63"); + sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63) + .await?; + } + if dbversion < 64 { + info!(context, "[migration] v64"); + sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64) + .await?; + } + if dbversion < 65 { + info!(context, "[migration] v65"); + sql.execute_migration( + r#" +ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER; +ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0; +ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#, + 65, + ) + .await?; + } + if dbversion < 66 { + info!(context, "[migration] v66"); + update_icons = true; + sql.set_db_version(66).await?; + } + if dbversion < 67 { + info!(context, "[migration] v67"); + for prefix in &["", "configured_"] { + if let Some(server_flags) = sql + .get_raw_config_int(format!("{}server_flags", prefix)) + .await? + { + let imap_socket_flags = server_flags & 0x700; + let key = format!("{}mail_security", prefix); + match imap_socket_flags { + 0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS + 0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS + 0x400 => sql.set_raw_config_int(key, 3).await?, // Plain + _ => sql.set_raw_config_int(key, 0).await?, + } + let smtp_socket_flags = server_flags & 0x70000; + let key = format!("{}send_security", prefix); + match smtp_socket_flags { + 0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS + 0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS + 0x40000 => sql.set_raw_config_int(key, 3).await?, // Plain + _ => sql.set_raw_config_int(key, 0).await?, + } + } + } + sql.set_db_version(67).await?; + } + if dbversion < 68 { + info!(context, "[migration] v68"); + // the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat() + sql.execute_migration( + "CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);", + 68, + ) + .await?; + } + if dbversion < 69 { + info!(context, "[migration] v69"); + sql.execute_migration( + r#" +ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0; +-- 120=group, 130=old verified group +UPDATE chats SET protected=1, type=120 WHERE type=130;"#, + 69, + ) + .await?; + } + + if dbversion < 71 { + info!(context, "[migration] v71"); + if let Some(addr) = context.get_config(Config::ConfiguredAddr).await? { + if let Ok(domain) = addr.parse::().map(|email| email.domain) { + context + .set_config( + Config::ConfiguredProvider, + get_provider_by_domain(&domain).map(|provider| provider.id), + ) + .await?; + } else { + warn!(context, "Can't parse configured address: {:?}", addr); + } + } + + sql.set_db_version(71).await?; + } + if dbversion < 72 { + info!(context, "[migration] v72"); + if !sql.col_exists("msgs", "mime_modified").await? { + sql.execute_migration( + r#" +ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#, + 72, + ) + .await?; + } + } + if dbversion < 73 { + use Config::*; + info!(context, "[migration] v73"); + sql.execute( + r#" +CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#, + ) + .await?; + for c in &[ + ConfiguredInboxFolder, + ConfiguredSentboxFolder, + ConfiguredMvboxFolder, + ] { + if let Some(folder) = context.get_config(*c).await? { + let (uid_validity, last_seen_uid) = + imap::get_config_last_seen_uid(context, &folder).await?; + if last_seen_uid > 0 { + imap::set_uid_next(context, &folder, last_seen_uid + 1).await?; + imap::set_uidvalidity(context, &folder, uid_validity).await?; + } + } + } + if exists_before_update { + disable_server_delete = true; + + // Don't disable server delete if it was on by default (Nauta): + if let Some(provider) = context.get_configured_provider().await? { + if let Some(defaults) = &provider.config_defaults { + if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) { + disable_server_delete = false; + } + } + } + } + sql.set_db_version(73).await?; + } + if dbversion < 74 { + info!(context, "[migration] v74"); + sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74) + .await?; + } + if dbversion < 75 { + info!(context, "[migration] v75"); + sql.execute_migration( + "ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';", + 74, + ) + .await?; + } + if dbversion < 76 { + info!(context, "[migration] v76"); + sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76) + .await?; + } + + Ok((recalc_fingerprints, update_icons, disable_server_delete)) +} + +impl Sql { + async fn set_db_version(&self, version: i32) -> Result<()> { + self.set_raw_config_int(VERSION_CFG, version).await?; + Ok(()) + } + + async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> { + let query = sqlx::query(query); + self.transaction(move |conn| { + Box::pin(async move { + query + .execute_many(&mut *conn) + .await + .collect::, _>>() + .await?; + + // set raw config inside the transaction + sqlx::query("UPDATE config SET value=? WHERE keyname=?;") + .bind(format!("{}", version)) + .bind(VERSION_CFG) + .execute(&mut *conn) + .await?; + + Ok(()) + }) + }) + .await?; + + Ok(()) + } +} diff --git a/src/sql/mod.rs b/src/sql/mod.rs new file mode 100644 index 000000000..5ce93f3ca --- /dev/null +++ b/src/sql/mod.rs @@ -0,0 +1,866 @@ +//! # SQLite wrapper + +use std::collections::HashSet; +use std::path::Path; +use std::pin::Pin; +use std::time::Duration; + +use anyhow::Context as _; +use async_std::prelude::*; +use async_std::sync::RwLock; +use sqlx::{ + pool::PoolOptions, + sqlite::{Sqlite, SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous}, + Execute, Executor, Row, +}; + +use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; +use crate::config::Config; +use crate::constants::{Viewtype, DC_CHAT_ID_TRASH}; +use crate::context::Context; +use crate::dc_tools::{dc_delete_file, time}; +use crate::ephemeral::start_ephemeral_timers; +use crate::message::Message; +use crate::param::{Param, Params}; +use crate::peerstate::Peerstate; +use crate::stock_str; + +mod error; +mod migrations; + +pub use self::error::*; + +/// A wrapper around the underlying Sqlite3 object. +/// +/// We maintain two different pools to sqlite, on for reading, one for writing. +/// This can go away once https://github.com/launchbadge/sqlx/issues/459 is implemented. +#[derive(Debug)] +pub struct Sql { + /// Writer pool, must only have 1 connection in it. + writer: RwLock>, + /// Reader pool, maintains multiple connections for reading data. + reader: RwLock>, +} + +impl Default for Sql { + fn default() -> Self { + Self { + writer: RwLock::new(None), + reader: RwLock::new(None), + } + } +} + +impl Drop for Sql { + fn drop(&mut self) { + async_std::task::block_on(self.close()); + } +} + +impl Sql { + pub fn new() -> Sql { + Self::default() + } + + /// Checks if there is currently a connection to the underlying Sqlite database. + pub async fn is_open(&self) -> bool { + // in read only mode the writer does not exists + self.reader.read().await.is_some() + } + + /// Closes all underlying Sqlite connections. + pub async fn close(&self) { + if let Some(sql) = self.writer.write().await.take() { + sql.close().await; + } + if let Some(sql) = self.reader.write().await.take() { + sql.close().await; + } + } + + async fn new_writer_pool(dbfile: impl AsRef) -> sqlx::Result { + let config = SqliteConnectOptions::new() + .journal_mode(SqliteJournalMode::Wal) + .filename(dbfile.as_ref()) + .read_only(false) + .busy_timeout(Duration::from_secs(100)) + .create_if_missing(true) + .statement_cache_capacity(0) // XXX workaround for https://github.com/launchbadge/sqlx/issues/1147 + .synchronous(SqliteSynchronous::Normal); + + PoolOptions::::new() + .max_connections(1) + .after_connect(|conn| { + Box::pin(async move { + let q = r#" +PRAGMA secure_delete=on; +PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android +"#; + + conn.execute_many(sqlx::query(q)) + .collect::, _>>() + .await?; + Ok(()) + }) + }) + .connect_with(config) + .await + } + + async fn new_reader_pool(dbfile: impl AsRef, readonly: bool) -> sqlx::Result { + let config = SqliteConnectOptions::new() + .journal_mode(SqliteJournalMode::Wal) + .filename(dbfile.as_ref()) + .read_only(readonly) + .busy_timeout(Duration::from_secs(100)) + .statement_cache_capacity(0) // XXX workaround for https://github.com/launchbadge/sqlx/issues/1147 + .synchronous(SqliteSynchronous::Normal); + + PoolOptions::::new() + .max_connections(10) + .after_connect(|conn| { + Box::pin(async move { + let q = r#" +PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android +PRAGMA query_only=1; -- Protect against writes even in read-write mode +"#; + + conn.execute_many(sqlx::query(q)) + .collect::, _>>() + .await?; + Ok(()) + }) + }) + .connect_with(config) + .await + } + + /// Opens the provided database and runs any necessary migrations. + /// If a database is already open, this will return an error. + pub async fn open( + &self, + context: &Context, + dbfile: impl AsRef, + readonly: bool, + ) -> anyhow::Result<()> { + if self.is_open().await { + error!( + context, + "Cannot open, database \"{:?}\" already opened.", + dbfile.as_ref(), + ); + return Err(Error::SqlAlreadyOpen.into()); + } + + // Open write pool + if !readonly { + *self.writer.write().await = Some(Self::new_writer_pool(&dbfile).await?); + } + + // Open read pool + *self.reader.write().await = Some(Self::new_reader_pool(&dbfile, readonly).await?); + + if !readonly { + // (1) update low-level database structure. + // this should be done before updates that use high-level objects that + // rely themselves on the low-level structure. + + let (recalc_fingerprints, update_icons, disable_server_delete) = + migrations::run(context, self).await?; + + // (2) updates that require high-level objects + // the structure is complete now and all objects are usable + + if recalc_fingerprints { + info!(context, "[migration] recalc fingerprints"); + let mut rows = self.fetch("SELECT addr FROM acpeerstates;").await?; + + while let Some(row) = rows.next().await { + let row = row?; + let addr = row.try_get(0)?; + if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? { + peerstate.recalc_fingerprint(); + peerstate.save_to_db(self, false).await?; + } + } + } + + if update_icons { + update_saved_messages_icon(context).await?; + update_device_icon(context).await?; + } + + if disable_server_delete { + // We now always watch all folders and delete messages there if delete_server is enabled. + // So, for people who have delete_server enabled, disable it and add a hint to the devicechat: + if context.get_config_delete_server_after().await?.is_some() { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(stock_str::delete_server_turned_off(context).await); + add_device_msg(context, None, Some(&mut msg)).await?; + context + .set_config(Config::DeleteServerAfter, Some("0")) + .await?; + } + } + } + + info!(context, "Opened {:?}.", dbfile.as_ref()); + + Ok(()) + } + + /// Execute the given query, returning the number of affected rows. + pub async fn execute<'e, 'q, E>(&self, query: E) -> Result + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let lock = self.writer.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let rows = pool.execute(query).await?; + Ok(rows.rows_affected()) + } + + /// Execute many queries. + pub async fn execute_many<'e, 'q, E>(&self, query: E) -> Result<()> + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let lock = self.writer.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + pool.execute_many(query) + .collect::>>() + .await?; + Ok(()) + } + + /// Fetch the given query. + pub async fn fetch<'e, 'q, E>( + &self, + query: E, + ) -> Result::Row>> + 'e + Send> + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let lock = self.reader.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let rows = pool.fetch(query); + Ok(rows) + } + + /// Fetch exactly one row, errors if no row is found. + pub async fn fetch_one<'e, 'q, E>(&self, query: E) -> Result<::Row> + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let lock = self.reader.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let row = pool.fetch_one(query).await?; + Ok(row) + } + + /// Fetches at most one row. + pub async fn fetch_optional<'e, 'q, E>( + &self, + query: E, + ) -> Result::Row>> + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let lock = self.reader.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let row = pool.fetch_optional(query).await?; + Ok(row) + } + + /// Used for executing `SELECT COUNT` statements only. Returns the resulting count. + pub async fn count<'e, 'q, E>(&self, query: E) -> Result + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + use std::convert::TryFrom; + + let row = self.fetch_one(query).await?; + let count: i64 = row.try_get(0)?; + + Ok(usize::try_from(count).map_err::(Into::into)?) + } + + /// Used for executing `SELECT COUNT` statements only. Returns `true`, if the count is at least + /// one, `false` otherwise. + pub async fn exists<'e, 'q, E>(&self, query: E) -> Result + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + { + let count = self.count(query).await?; + Ok(count > 0) + } + + /// Execute the function inside a transaction. + /// + /// If the function returns an error, the transaction will be rolled back. If it does not return an + /// error, the transaction will be committed. + pub async fn transaction(&self, callback: F) -> Result + where + F: for<'c> FnOnce( + &'c mut sqlx::Transaction<'_, Sqlite>, + ) -> Pin> + 'c + Send>> + + 'static + + Send + + Sync, + R: Send, + { + let lock = self.writer.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let mut transaction = pool.begin().await?; + let ret = callback(&mut transaction).await; + + match ret { + Ok(ret) => { + transaction.commit().await?; + + Ok(ret) + } + Err(err) => { + transaction.rollback().await?; + + Err(err) + } + } + } + + /// Query the database if the requested table already exists. + pub async fn table_exists(&self, name: impl AsRef) -> Result { + let q = format!("PRAGMA table_info(\"{}\")", name.as_ref()); + + let lock = self.reader.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let mut rows = pool.fetch(sqlx::query(&q)); + if let Some(first_row) = rows.next().await { + Ok(first_row.is_ok()) + } else { + Ok(false) + } + } + + /// Check if a column exists in a given table. + pub async fn col_exists( + &self, + table_name: impl AsRef, + col_name: impl AsRef, + ) -> Result { + let q = format!("PRAGMA table_info(\"{}\")", table_name.as_ref()); + let lock = self.reader.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + + let mut rows = pool.fetch(sqlx::query(&q)); + while let Some(row) = rows.next().await { + let row = row?; + + // `PRAGMA table_info` returns one row per column, + // each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value + + let curr_name: &str = row.try_get(1)?; + if col_name.as_ref() == curr_name { + return Ok(true); + } + } + + Ok(false) + } + + /// Executes a query which is expected to return one row and one + /// column. If the query does not return a value or returns SQL + /// `NULL`, returns `Ok(None)`. + pub async fn query_get_value<'e, 'q, E, T>(&self, query: E) -> Result> + where + 'q: 'e, + E: 'q + Execute<'q, Sqlite>, + T: for<'r> sqlx::Decode<'r, Sqlite> + sqlx::Type, + { + let res = self + .fetch_optional(query) + .await? + .map(|row| row.get::(0)); + Ok(res) + } + + /// Set private configuration options. + /// + /// Setting `None` deletes the value. On failure an error message + /// will already have been logged. + pub async fn set_raw_config(&self, key: impl AsRef, value: Option<&str>) -> Result<()> { + if !self.is_open().await { + return Err(Error::SqlNoConnection); + } + + let key = key.as_ref(); + if let Some(value) = value { + let exists = self + .exists(sqlx::query("SELECT COUNT(*) FROM config WHERE keyname=?;").bind(key)) + .await?; + + if exists { + self.execute( + sqlx::query("UPDATE config SET value=? WHERE keyname=?;") + .bind(value) + .bind(key), + ) + .await?; + } else { + self.execute( + sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") + .bind(key) + .bind(value), + ) + .await?; + } + } else { + self.execute(sqlx::query("DELETE FROM config WHERE keyname=?;").bind(key)) + .await?; + } + + Ok(()) + } + + /// Get configuration options from the database. + pub async fn get_raw_config(&self, key: impl AsRef) -> Result> { + if !self.is_open().await || key.as_ref().is_empty() { + return Err(Error::SqlNoConnection); + } + let value = self + .query_get_value( + sqlx::query("SELECT value FROM config WHERE keyname=?;").bind(key.as_ref()), + ) + .await + .context(format!("failed to fetch raw config: {}", key.as_ref()))?; + + Ok(value) + } + + pub async fn set_raw_config_int(&self, key: impl AsRef, value: i32) -> Result<()> { + self.set_raw_config(key, Some(&format!("{}", value))).await + } + + pub async fn get_raw_config_int(&self, key: impl AsRef) -> Result> { + self.get_raw_config(key) + .await + .map(|s| s.and_then(|s| s.parse().ok())) + } + + pub async fn get_raw_config_bool(&self, key: impl AsRef) -> Result { + // Not the most obvious way to encode bool as string, but it is matter + // of backward compatibility. + let res = self.get_raw_config_int(key).await?; + Ok(res.unwrap_or_default() > 0) + } + + pub async fn set_raw_config_bool(&self, key: T, value: bool) -> Result<()> + where + T: AsRef, + { + let value = if value { Some("1") } else { None }; + self.set_raw_config(key, value).await + } + + pub async fn set_raw_config_int64(&self, key: impl AsRef, value: i64) -> Result<()> { + self.set_raw_config(key, Some(&format!("{}", value))).await + } + + pub async fn get_raw_config_int64(&self, key: impl AsRef) -> Result> { + self.get_raw_config(key) + .await + .map(|s| s.and_then(|r| r.parse().ok())) + } + + /// Alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above. + /// the ORDER BY ensures, this function always returns the most recent id, + /// eg. if a Message-ID is split into different messages. + pub async fn get_rowid( + &self, + table: impl AsRef, + field: impl AsRef, + value: impl AsRef, + ) -> 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. + let query = format!( + "SELECT id FROM {} WHERE {}=? ORDER BY id DESC", + table.as_ref(), + field.as_ref(), + ); + + self.query_get_value(sqlx::query(&query).bind(value.as_ref())) + .await + .map(|id| id.unwrap_or_default()) + } + + /// Fetches the rowid by restricting the rows through two different key, value settings. + pub async fn get_rowid2( + &self, + table: impl AsRef, + field: impl AsRef, + value: i64, + field2: impl AsRef, + value2: i64, + ) -> Result { + let query = format!( + "SELECT id FROM {} WHERE {}={} AND {}={} ORDER BY id DESC", + table.as_ref(), + field.as_ref(), + value, + field2.as_ref(), + value2, + ); + + self.query_get_value(sqlx::query(&query)) + .await + .map(|id| id.unwrap_or_default()) + } +} + +pub async fn housekeeping(context: &Context) -> Result<()> { + if let Err(err) = crate::ephemeral::delete_expired_messages(context).await { + warn!(context, "Failed to delete expired messages: {}", err); + } + + let mut files_in_use = HashSet::new(); + let mut unreferenced_count = 0; + + info!(context, "Start housekeeping..."); + maybe_add_from_param( + &context.sql, + &mut files_in_use, + "SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;", + Param::File, + ) + .await?; + maybe_add_from_param( + &context.sql, + &mut files_in_use, + "SELECT param FROM jobs;", + Param::File, + ) + .await?; + maybe_add_from_param( + &context.sql, + &mut files_in_use, + "SELECT param FROM chats;", + Param::ProfileImage, + ) + .await?; + maybe_add_from_param( + &context.sql, + &mut files_in_use, + "SELECT param FROM contacts;", + Param::ProfileImage, + ) + .await?; + + let mut rows = context.sql.fetch("SELECT value FROM config;").await?; + while let Some(row) = rows.next().await { + let row: String = row?.try_get(0)?; + maybe_add_file(&mut files_in_use, row); + } + + info!(context, "{} files in use.", files_in_use.len(),); + /* go through directory and delete unused files */ + let p = context.get_blobdir(); + match async_std::fs::read_dir(p).await { + Ok(mut dir_handle) => { + /* avoid deletion of files that are just created to build a message object */ + let diff = std::time::Duration::from_secs(60 * 60); + let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap(); + + while let Some(entry) = dir_handle.next().await { + if entry.is_err() { + break; + } + let entry = entry.unwrap(); + let name_f = entry.file_name(); + let name_s = name_f.to_string_lossy(); + + if is_file_in_use(&files_in_use, None, &name_s) + || is_file_in_use(&files_in_use, Some(".increation"), &name_s) + || is_file_in_use(&files_in_use, Some(".waveform"), &name_s) + || is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s) + { + continue; + } + + unreferenced_count += 1; + + if let Ok(stats) = async_std::fs::metadata(entry.path()).await { + let recently_created = + stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than; + let recently_modified = stats.modified().is_ok() + && stats.modified().unwrap() > keep_files_newer_than; + let recently_accessed = stats.accessed().is_ok() + && stats.accessed().unwrap() > keep_files_newer_than; + + if recently_created || recently_modified || recently_accessed { + info!( + context, + "Housekeeping: Keeping new unreferenced file #{}: {:?}", + unreferenced_count, + entry.file_name(), + ); + continue; + } + } + info!( + context, + "Housekeeping: Deleting unreferenced file #{}: {:?}", + unreferenced_count, + entry.file_name() + ); + let path = entry.path(); + dc_delete_file(context, path).await; + } + } + Err(err) => { + warn!( + context, + "Housekeeping: Cannot open {}. ({})", + context.get_blobdir().display(), + err + ); + } + } + + if let Err(err) = start_ephemeral_timers(context).await { + warn!( + context, + "Housekeeping: cannot start ephemeral timers: {}", err + ); + } + + if let Err(err) = prune_tombstones(&context.sql).await { + warn!( + context, + "Housekeeping: Cannot prune message tombstones: {}", err + ); + } + + if let Err(e) = context + .set_config(Config::LastHousekeeping, Some(&time().to_string())) + .await + { + warn!(context, "Can't set config: {}", e); + } + + info!(context, "Housekeeping done."); + Ok(()) +} + +#[allow(clippy::indexing_slicing)] +fn is_file_in_use(files_in_use: &HashSet, namespc_opt: Option<&str>, name: &str) -> bool { + let name_to_check = if let Some(namespc) = namespc_opt { + let name_len = name.len(); + let namespc_len = namespc.len(); + if name_len <= namespc_len || !name.ends_with(namespc) { + return false; + } + &name[..name_len - namespc_len] + } else { + name + }; + files_in_use.contains(name_to_check) +} + +fn maybe_add_file(files_in_use: &mut HashSet, file: impl AsRef) { + if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") { + files_in_use.insert(file.to_string()); + } +} + +async fn maybe_add_from_param( + sql: &Sql, + files_in_use: &mut HashSet, + query: &str, + param_id: Param, +) -> Result<()> { + let mut rows = sql.fetch(query).await?; + while let Some(row) = rows.next().await { + let row: String = row?.try_get(0)?; + let param: Params = row.parse().unwrap_or_default(); + if let Some(file) = param.get(param_id) { + maybe_add_file(files_in_use, file); + } + } + + Ok(()) +} + +/// Removes from the database locally deleted messages that also don't +/// have a server UID. +async fn prune_tombstones(sql: &Sql) -> Result<()> { + sql.execute( + sqlx::query( + "DELETE FROM msgs \ + WHERE (chat_id = ? OR hidden) \ + AND server_uid = 0", + ) + .bind(DC_CHAT_ID_TRASH), + ) + .await?; + Ok(()) +} + +/// Returns the SQLite version as a string; e.g., `"3.16.2"` for version 3.16.2. +pub fn version() -> &'static str { + #[allow(unsafe_code)] + let cstr = unsafe { std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion()) }; + cstr.to_str() + .expect("SQLite version string is not valid UTF8 ?!") +} + +#[cfg(test)] +mod test { + use async_std::fs::File; + + use crate::config::Config; + use crate::{test_utils::TestContext, Event, EventType}; + + use super::*; + + #[test] + fn test_maybe_add_file() { + let mut files = Default::default(); + maybe_add_file(&mut files, "$BLOBDIR/hello"); + maybe_add_file(&mut files, "$BLOBDIR/world.txt"); + maybe_add_file(&mut files, "world2.txt"); + maybe_add_file(&mut files, "$BLOBDIR"); + + assert!(files.contains("hello")); + assert!(files.contains("world.txt")); + assert!(!files.contains("world2.txt")); + assert!(!files.contains("$BLOBDIR")); + } + + #[test] + fn test_is_file_in_use() { + let mut files = Default::default(); + maybe_add_file(&mut files, "$BLOBDIR/hello"); + maybe_add_file(&mut files, "$BLOBDIR/world.txt"); + maybe_add_file(&mut files, "world2.txt"); + + assert!(is_file_in_use(&files, None, "hello")); + assert!(!is_file_in_use(&files, Some(".txt"), "hello")); + assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix")); + } + + #[async_std::test] + async fn test_table_exists() { + let t = TestContext::new().await; + assert!(t.ctx.sql.table_exists("msgs").await.unwrap()); + assert!(!t.ctx.sql.table_exists("foobar").await.unwrap()); + } + + #[async_std::test] + async fn test_col_exists() { + let t = TestContext::new().await; + assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap()); + assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap()); + assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap()); + } + + #[async_std::test] + async fn test_housekeeping_db_closed() { + let t = TestContext::new().await; + + let avatar_src = t.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + File::create(&avatar_src) + .await + .unwrap() + .write_all(avatar_bytes) + .await + .unwrap(); + t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await + .unwrap(); + + t.add_event_sink(move |event: Event| async move { + match event.typ { + EventType::Info(s) => assert!( + !s.contains("Keeping new unreferenced file"), + "File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)", + s + ), + EventType::Error(s) => panic!(s), + _ => {} + } + }) + .await; + + let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); + + t.sql.close().await; + housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed + t.sql.open(&t, &t.get_dbfile(), false).await.unwrap(); + + let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); + } + + /// Regression test. + /// + /// Previously the code checking for existence of `config` table + /// checked it with `PRAGMA table_info("config")` but did not + /// drain `SqlitePool.fetch` result, only using the first row + /// returned. As a result, prepared statement for `PRAGMA` was not + /// finalized early enough, leaving reader connection in a broken + /// state after reopening the database, when `config` table + /// existed and `PRAGMA` returned non-empty result. + /// + /// Statements were not finalized due to a bug in sqlx: + /// https://github.com/launchbadge/sqlx/issues/1147 + #[async_std::test] + async fn test_db_reopen() -> Result<()> { + use tempfile::tempdir; + + // The context is used only for logging. + let t = TestContext::new().await; + + // Create a separate empty database for testing. + let dir = tempdir()?; + let dbfile = dir.path().join("testdb.sqlite"); + let sql = Sql::new(); + + // Create database with all the tables. + sql.open(&t, &dbfile, false).await.unwrap(); + sql.close().await; + + // Reopen the database + sql.open(&t, &dbfile, false).await?; + sql.execute( + sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") + .bind("foo") + .bind("bar"), + ) + .await?; + + let value: Option = sql + .query_get_value(sqlx::query("SELECT value FROM config WHERE keyname=?;").bind("foo")) + .await?; + assert_eq!(value.unwrap(), "bar"); + + Ok(()) + } +} diff --git a/src/sql/tables.sql b/src/sql/tables.sql new file mode 100644 index 000000000..bb892b829 --- /dev/null +++ b/src/sql/tables.sql @@ -0,0 +1,185 @@ +CREATE TABLE config ( + id INTEGER PRIMARY KEY, + keyname TEXT, + value TEXT +); +CREATE INDEX config_index1 ON config (keyname); +CREATE TABLE contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT DEFAULT '', + addr TEXT DEFAULT '' COLLATE NOCASE, + origin INTEGER DEFAULT 0, + blocked INTEGER DEFAULT 0, + last_seen INTEGER DEFAULT 0, + param TEXT DEFAULT '', + authname TEXT DEFAULT '', + selfavatar_sent INTEGER DEFAULT 0 +); +CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE); +CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE); +INSERT INTO contacts (id,name,origin) VALUES +(1,'self',262144), (2,'info',262144), (3,'rsvd',262144), +(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144), +(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144); + +CREATE TABLE chats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER DEFAULT 0, + name TEXT DEFAULT '', + draft_timestamp INTEGER DEFAULT 0, + draft_txt TEXT DEFAULT '', + blocked INTEGER DEFAULT 0, + grpid TEXT DEFAULT '', + param TEXT DEFAULT '', + archived INTEGER DEFAULT 0, + gossiped_timestamp INTEGER DEFAULT 0, + locations_send_begin INTEGER DEFAULT 0, + locations_send_until INTEGER DEFAULT 0, + locations_last_sent INTEGER DEFAULT 0, + created_timestamp INTEGER DEFAULT 0, + muted_until INTEGER DEFAULT 0, + ephemeral_timer INTEGER +); +CREATE INDEX chats_index1 ON chats (grpid); +CREATE INDEX chats_index2 ON chats (archived); +CREATE INDEX chats_index3 ON chats (locations_send_until); +INSERT INTO chats (id,type,name) VALUES +(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'), +(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'), +(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd'); + +CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER); +CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id); +CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id); + +CREATE TABLE msgs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rfc724_mid TEXT DEFAULT '', + server_folder TEXT DEFAULT '', + server_uid INTEGER DEFAULT 0, + chat_id INTEGER DEFAULT 0, + from_id INTEGER DEFAULT 0, + to_id INTEGER DEFAULT 0, + timestamp INTEGER DEFAULT 0, + type INTEGER DEFAULT 0, + state INTEGER DEFAULT 0, + msgrmsg INTEGER DEFAULT 1, + bytes INTEGER DEFAULT 0, + txt TEXT DEFAULT '', + txt_raw TEXT DEFAULT '', + param TEXT DEFAULT '', + starred INTEGER DEFAULT 0, + timestamp_sent INTEGER DEFAULT 0, + timestamp_rcvd INTEGER DEFAULT 0, + hidden INTEGER DEFAULT 0, + mime_headers TEXT, + mime_in_reply_to TEXT, + mime_references TEXT, + move_state INTEGER DEFAULT 1, + location_id INTEGER DEFAULT 0, + error TEXT DEFAULT '', + +-- Timer value in seconds. For incoming messages this +-- timer starts when message is read, so we want to have +-- the value stored here until the timer starts. + ephemeral_timer INTEGER DEFAULT 0, + +-- Timestamp indicating when the message should be +-- deleted. It is convenient to store it here because UI +-- needs this value to display how much time is left until +-- the message is deleted. + ephemeral_timestamp INTEGER DEFAULT 0 +); + +CREATE INDEX msgs_index1 ON msgs (rfc724_mid); +CREATE INDEX msgs_index2 ON msgs (chat_id); +CREATE INDEX msgs_index3 ON msgs (timestamp); +CREATE INDEX msgs_index4 ON msgs (state); +CREATE INDEX msgs_index5 ON msgs (starred); +CREATE INDEX msgs_index6 ON msgs (location_id); +CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id); +INSERT INTO msgs (id,msgrmsg,txt) VALUES +(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'), +(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'), +(8,0,'rsvd'), (9,0,'daymarker'); + +CREATE TABLE jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + added_timestamp INTEGER, + desired_timestamp INTEGER DEFAULT 0, + action INTEGER, + foreign_id INTEGER, + param TEXT DEFAULT '', + thread INTEGER DEFAULT 0, + tries INTEGER DEFAULT 0 +); +CREATE INDEX jobs_index1 ON jobs (desired_timestamp); + +CREATE TABLE leftgrps ( + id INTEGER PRIMARY KEY, + grpid TEXT DEFAULT '' +); +CREATE INDEX leftgrps_index1 ON leftgrps (grpid); + +CREATE TABLE keypairs ( + id INTEGER PRIMARY KEY, + addr TEXT DEFAULT '' COLLATE NOCASE, + is_default INTEGER DEFAULT 0, + private_key, + public_key, + created INTEGER DEFAULT 0 +); + +CREATE TABLE acpeerstates ( + id INTEGER PRIMARY KEY, + addr TEXT DEFAULT '' COLLATE NOCASE, + last_seen INTEGER DEFAULT 0, + last_seen_autocrypt INTEGER DEFAULT 0, + public_key, + prefer_encrypted INTEGER DEFAULT 0, + gossip_timestamp INTEGER DEFAULT 0, + gossip_key, + public_key_fingerprint TEXT DEFAULT '', + gossip_key_fingerprint TEXT DEFAULT '', + verified_key, + verified_key_fingerprint TEXT DEFAULT '' +); +CREATE INDEX acpeerstates_index1 ON acpeerstates (addr); +CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint); +CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint); +CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint); + +CREATE TABLE msgs_mdns ( + msg_id INTEGER, + contact_id INTEGER, + timestamp_sent INTEGER DEFAULT 0 +); +CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id); + +CREATE TABLE tokens ( + id INTEGER PRIMARY KEY, + namespc INTEGER DEFAULT 0, + foreign_id INTEGER DEFAULT 0, + token TEXT DEFAULT '', + timestamp INTEGER DEFAULT 0 +); + +CREATE TABLE locations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + latitude REAL DEFAULT 0.0, + longitude REAL DEFAULT 0.0, + accuracy REAL DEFAULT 0.0, + timestamp INTEGER DEFAULT 0, + chat_id INTEGER DEFAULT 0, + from_id INTEGER DEFAULT 0, + independent INTEGER DEFAULT 0 +); +CREATE INDEX locations_index1 ON locations (from_id); +CREATE INDEX locations_index2 ON locations (timestamp); + +CREATE TABLE devmsglabels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT, + msg_id INTEGER DEFAULT 0 +); +CREATE INDEX devmsglabels_index1 ON devmsglabels (label); diff --git a/src/stock_str.rs b/src/stock_str.rs index 14ad7763d..623d75baf 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -898,15 +898,15 @@ impl Context { } pub(crate) async fn update_device_chats(&self) -> Result<(), Error> { - if self.get_config_bool(Config::Bot).await { + if self.get_config_bool(Config::Bot).await? { return Ok(()); } // create saved-messages chat; we do this only once, if the user has deleted the chat, // he can recreate it manually (make sure we do not re-add it when configure() was called a second time) - if !self.sql.get_raw_config_bool(self, "self-chat-added").await { + if !self.sql.get_raw_config_bool("self-chat-added").await? { self.sql - .set_raw_config_bool(self, "self-chat-added", true) + .set_raw_config_bool("self-chat-added", true) .await?; chat::create_by_contact_id(self, DC_CONTACT_ID_SELF).await?; } @@ -1061,10 +1061,16 @@ mod tests { }; // delete self-talk first; this adds a message to device-chat about how self-talk can be restored - let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(); + let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap() + .len(); self_talk_id.delete(&t).await.ok(); assert_eq!( - chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(), + chat::get_chat_msgs(&t, device_chat_id, 0, None) + .await + .unwrap() + .len(), device_chat_msgs_before + 1 ); diff --git a/src/test_utils.rs b/src/test_utils.rs index 9e425015c..8af5058ec 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -15,6 +15,7 @@ use async_std::{channel, pin::Pin}; use async_std::{future::Future, task}; use chat::ChatItem; use once_cell::sync::Lazy; +use sqlx::Row; use tempfile::{tempdir, TempDir}; use crate::chat::{self, Chat, ChatId}; @@ -85,6 +86,7 @@ impl TestContext { async fn new_named(name: Option) -> Self { use rand::Rng; + pretty_env_logger::try_init().ok(); let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); @@ -95,9 +97,10 @@ impl TestContext { } let ctx = Context::new("FakeOS".into(), dbfile.into(), id) .await - .unwrap(); + .expect("failed to create context"); let events = ctx.get_event_emitter(); + let event_sinks: Arc>>> = Arc::new(RwLock::new(Vec::new())); let sinks = Arc::clone(&event_sinks); let (poison_sender, poison_receiver) = channel::bounded(1); @@ -114,6 +117,7 @@ impl TestContext { while let Some(event) = events.recv().await { { + log::debug!("{:?}", event); let sinks = sinks.read().await; for sink in sinks.iter() { sink(event.clone()).await; @@ -224,22 +228,25 @@ impl TestContext { let row = self .ctx .sql - .query_row( - r#" + .fetch_one( + sqlx::query( + r#" SELECT id, foreign_id, param FROM jobs WHERE action=? ORDER BY desired_timestamp DESC; "#, - paramsv![Action::SendMsgToSmtp], - |row| { - let id: i64 = row.get(0)?; - let foreign_id: i64 = row.get(1)?; - let param: String = row.get(2)?; - Ok((id, foreign_id, param)) - }, + ) + .bind(Action::SendMsgToSmtp), ) - .await; + .await + .and_then(|row| { + let id: u32 = row.try_get(0)?; + let foreign_id: u32 = row.try_get(1)?; + let param: String = row.try_get(2)?; + Ok((id, foreign_id, param)) + }); + if let Ok(row) = row { break row; } @@ -249,7 +256,7 @@ impl TestContext { panic!("no sent message found in jobs table"); } }; - let id = MsgId::new(foreign_id as u32); + let id = MsgId::new(foreign_id); let params = Params::from_str(&raw_params).unwrap(); let blob_path = params .get_blob(Param::File, &self.ctx, false) @@ -259,7 +266,7 @@ impl TestContext { .to_abs_path(); self.ctx .sql - .execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid]) + .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(rowid)) .await .expect("failed to remove job"); update_msg_state(&self.ctx, id, MessageState::OutDelivered).await; @@ -302,7 +309,9 @@ impl TestContext { /// /// Panics on errors or if the most recent message is a marker. pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message { - let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None) + .await + .unwrap(); let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { msg_id } else { @@ -313,13 +322,17 @@ impl TestContext { /// Gets the most recent message over all chats. pub async fn get_last_msg(&self) -> Message { - let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap(); + let chats = Chatlist::try_load(&self.ctx, 0, None, None) + .await + .expect("failed to load chatlist"); // 0 is correct in the next line (as opposed to `chats.len() - 1`, which would be the last element): // The chatlist describes what you see when you open DC, a list of chats and in each of them // the first words of the last message. To get the last message overall, we look at the chat at the top of the // list, which has the index 0. let msg_id = chats.get_msg_id(0).unwrap(); - Message::load_from_db(&self.ctx, msg_id).await.unwrap() + Message::load_from_db(&self.ctx, msg_id) + .await + .expect("failed to load msg") } /// Creates or returns an existing 1:1 [`Chat`] with another account. @@ -333,8 +346,14 @@ impl TestContext { .ctx .get_config(Config::Displayname) .await + .unwrap_or_default() .unwrap_or_default(), - other.ctx.get_config(Config::ConfiguredAddr).await.unwrap(), + other + .ctx + .get_config(Config::ConfiguredAddr) + .await + .unwrap() + .unwrap(), Origin::ManuallyCreated, ) .await @@ -394,7 +413,7 @@ impl TestContext { #[allow(dead_code)] #[allow(clippy::clippy::indexing_slicing)] pub async fn print_chat(&self, chat_id: ChatId) { - let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await; + let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await.unwrap(); let msglist: Vec = msglist .into_iter() .map(|x| match x { @@ -405,7 +424,7 @@ impl TestContext { .collect(); let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap(); - let members = chat::get_chat_contacts(self, sel_chat.id).await; + let members = chat::get_chat_contacts(self, sel_chat.id).await.unwrap(); let subtitle = if sel_chat.is_device_talk() { "device-talk".to_string() } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { @@ -428,7 +447,7 @@ impl TestContext { } else { "" }, - match sel_chat.get_profile_image(self).await { + match sel_chat.get_profile_image(self).await.unwrap() { Some(icon) => match icon.to_str() { Some(icon) => format!(" Icon: {}", icon), _ => " Icon: Err".to_string(), @@ -563,7 +582,7 @@ pub(crate) async fn get_chat_msg( index: usize, asserted_msgs_count: usize, ) -> Message { - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await; + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap(); assert_eq!(msgs.len(), asserted_msgs_count); let msg_id = if let ChatItem::Message { msg_id } = msgs[index] { msg_id diff --git a/src/token.rs b/src/token.rs index af4b2df98..84e49fc5b 100644 --- a/src/token.rs +++ b/src/token.rs @@ -4,17 +4,13 @@ //! //! Tokens are used in countermitm verification protocols. -use deltachat_derive::{FromSql, ToSql}; - use crate::chat::ChatId; use crate::context::Context; use crate::dc_tools::{dc_create_id, time}; /// Token namespace -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, -)] -#[repr(i32)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)] +#[repr(u32)] pub enum Namespace { Unknown = 0, Auth = 110, @@ -30,26 +26,36 @@ impl Default for Namespace { /// Creates a new token and saves it into the database. /// /// Returns created token. -pub async fn save(context: &Context, namespace: Namespace, chat: Option) -> String { +pub async fn save(context: &Context, namespace: Namespace, foreign_id: Option) -> String { let token = dc_create_id(); - match chat { - Some(chat_id) => context + match foreign_id { + Some(foreign_id) => context .sql .execute( - "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", - paramsv![namespace, chat_id, token, time()], + sqlx::query( + "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);" + ) + .bind(namespace) + .bind(foreign_id) + .bind(&token) + .bind(time()), ) .await .ok(), None => context .sql .execute( - "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);", - paramsv![namespace, token, time()], + sqlx::query( + "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);" + ) + .bind(namespace) + .bind(&token) + .bind(time()), ) .await .ok(), }; + token } @@ -57,50 +63,51 @@ pub async fn lookup( context: &Context, namespace: Namespace, chat: Option, -) -> Option { - match chat { +) -> crate::sql::Result> { + let token = match chat { Some(chat_id) => { context .sql - .query_get_value::( - context, - "SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;", - paramsv![namespace, chat_id], + .query_get_value( + sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;") + .bind(namespace) + .bind(chat_id), ) - .await + .await? } // foreign_id is declared as `INTEGER DEFAULT 0` in the schema. None => { context .sql - .query_get_value::( - context, - "SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;", - paramsv![namespace], + .query_get_value( + sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;") + .bind(namespace), ) - .await + .await? } - } + }; + Ok(token) } pub async fn lookup_or_new( context: &Context, namespace: Namespace, - chat: Option, + foreign_id: Option, ) -> String { - if let Some(token) = lookup(context, namespace, chat).await { + if let Ok(Some(token)) = lookup(context, namespace, foreign_id).await { return token; } - save(context, namespace, chat).await + save(context, namespace, foreign_id).await } pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> bool { context .sql .exists( - "SELECT id FROM tokens WHERE namespc=? AND token=?;", - paramsv![namespace, token], + sqlx::query("SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;") + .bind(namespace) + .bind(token), ) .await .unwrap_or_default()