mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df546b9d2e | ||
|
|
5c13d2322a | ||
|
|
2d5caf9d3e | ||
|
|
876e3ed58e | ||
|
|
cdb5f0d536 | ||
|
|
0d791bb6b3 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.45.0
|
||||
toolchain: 1.43.1
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
@@ -32,13 +32,12 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.45.0
|
||||
toolchain: 1.43.1
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all
|
||||
|
||||
|
||||
build_and_test:
|
||||
@@ -47,7 +46,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [nightly, 1.45.0]
|
||||
rust: [nightly, 1.43.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
@@ -80,10 +79,11 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests
|
||||
args: --workspace --all --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
args: --workspace
|
||||
|
||||
|
||||
54
ASYNC-API-TODO.txt
Normal file
54
ASYNC-API-TODO.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
|
||||
|
||||
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
|
||||
|
||||
APIs:
|
||||
dc_context_new # opens the database
|
||||
dc_open # FFI only
|
||||
-> drop it and move parameters to dc_context_new()
|
||||
|
||||
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
|
||||
dc_imex NEVER goes through the job system
|
||||
dc_imex import_backup needs to ensure dc_stop_jobs()
|
||||
|
||||
dc_start_io # start smtp/imap and job handling subsystems
|
||||
dc_stop_io # stop smtp/imap and job handling subsystems
|
||||
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
|
||||
|
||||
dc_close # FFI only
|
||||
-> can be dropped
|
||||
dc_context_unref
|
||||
|
||||
for ios share-extension:
|
||||
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
|
||||
0: message was sent
|
||||
1: message failed to go out, is queued as a job to be retried later
|
||||
2: message permanently failed?
|
||||
|
||||
EVENT handling:
|
||||
start a callback thread and call get_next_event() which is BLOCKING
|
||||
it's fine to start this callback thread later, it will see all events.
|
||||
Note that the core infinitely fills the internal queue if you never drain it.
|
||||
|
||||
FFI-get_next_event() returns NULL if the context is unrefed already?
|
||||
|
||||
sidenote: how python's callback thread does it currently:
|
||||
CB-thread runs this while loop:
|
||||
while not QUITFLAG:
|
||||
ev = context.get_next_event( )
|
||||
...
|
||||
So in order to shutdown properly one has to set QUITFLAG
|
||||
before calling dc_stop_jobs() and dc_context_unref
|
||||
|
||||
event API:
|
||||
get_data1_int
|
||||
get_data2_int
|
||||
get_data3_str
|
||||
|
||||
|
||||
- userdata likely only used for the callbacks, likely can be dropped, needs verification
|
||||
|
||||
|
||||
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,18 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 1.40.0
|
||||
|
||||
- introduce ephemeral messages #1540 #1680 #1683 #1684 #1691 #1692
|
||||
|
||||
- `DC_MSG_ID_DAYMARKER` gets timestamp attached #1677 #1685
|
||||
|
||||
- improve idle #1690 #1688
|
||||
|
||||
- fix message processing issues by sequential processing #1694
|
||||
|
||||
- refactorings #1670 #1673
|
||||
|
||||
|
||||
## 1.39.0
|
||||
|
||||
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
|
||||
|
||||
331
Cargo.lock
generated
331
Cargo.lock
generated
@@ -103,10 +103,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.10"
|
||||
name = "ahash"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
|
||||
checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -290,6 +296,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0afb7287b68575f5ca0e5c7e40191cbd4be59d325781f46faa603e176eaef47"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -549,6 +564,15 @@ dependencies = [
|
||||
"iovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.1.0"
|
||||
@@ -714,6 +738,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.2.3"
|
||||
@@ -822,7 +856,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.40.0"
|
||||
version = "1.39.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"anyhow",
|
||||
@@ -852,6 +886,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"mailparse",
|
||||
"native-tls",
|
||||
@@ -863,11 +898,8 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"proptest",
|
||||
"quick-xml",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"rustyline",
|
||||
"sanitize-filename",
|
||||
"serde",
|
||||
@@ -875,6 +907,7 @@ dependencies = [
|
||||
"sha2 0.9.0",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sqlx",
|
||||
"stop-token",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
@@ -894,7 +927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.40.0"
|
||||
version = "1.39.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -983,6 +1016,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 = "dtoa"
|
||||
version = "0.4.6"
|
||||
@@ -1165,18 +1204,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fast_chemail"
|
||||
version = "0.9.6"
|
||||
@@ -1344,6 +1371,19 @@ dependencies = [
|
||||
"tokio-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustc_version",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.3"
|
||||
@@ -1418,6 +1458,16 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab9b7860757ce258c89fd48d28b68c41713e597a7b09e793f6c6a6e2ea37c827"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"autocfg 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
@@ -1789,6 +1839,17 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls 0.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
@@ -1815,6 +1876,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"
|
||||
@@ -2115,9 +2182,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7fad362df89617628a7508b3e9d588ade1b0ac31aa25de168193ad999c2dd4"
|
||||
checksum = "1bcaa58ee64f8e4a3d02f5d8e6ed0340eae28fed6fdabd984ad1776e3b43848a"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -2208,6 +2275,50 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "0.4.22"
|
||||
@@ -2376,27 +2487,6 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2_sqlite"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed60ebe88b27ac28c0563bc0fbeaecd302ff53e3a01e5ddc2ec9f4e6c707d929"
|
||||
dependencies = [
|
||||
"r2d2",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
@@ -2422,6 +2512,7 @@ dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
"rand_pcg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2467,6 +2558,15 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.2.0"
|
||||
@@ -2593,22 +2693,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45d0fd62e1df63d254714e6cb40d0a0e82e7a1623e7a27f679d851af092ae58b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"libsqlite3-sys",
|
||||
"lru-cache",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
"time 0.1.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.7.0"
|
||||
@@ -2707,13 +2791,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.4"
|
||||
name = "scoped-tls"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
@@ -2805,9 +2886,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6e32b85107a5c8062643265a90575cc6e798cec0906ea58519b42175062ba27"
|
||||
checksum = "c6f3acf84e23ab27c01cb5917551765c01c50b2000089db8fa47fe018a3260cf"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"error-chain",
|
||||
@@ -2893,6 +2974,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"
|
||||
|
||||
[[package]]
|
||||
name = "skeptic"
|
||||
version = "0.13.4"
|
||||
@@ -2935,7 +3022,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"scoped-tls",
|
||||
"scoped-tls 1.0.0",
|
||||
"slab",
|
||||
"socket2",
|
||||
"wepoll-sys-stjepang",
|
||||
@@ -2960,6 +3047,88 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "sqlformat"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce64a4576e1720a2e511bf3ccdb8c0f6cfed0fc265bcbaa0bd369485e02c631"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"maplit",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.4.0-pre"
|
||||
source = "git+https://github.com/launchbadge/sqlx#72c4e040bccfb0689529fa574124e35947c18236"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.4.0-pre"
|
||||
source = "git+https://github.com/launchbadge/sqlx#72c4e040bccfb0689529fa574124e35947c18236"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes 0.5.5",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"either",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
"hex",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"phf",
|
||||
"smallvec",
|
||||
"sqlformat",
|
||||
"sqlx-rt",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"url",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.4.0-pre"
|
||||
source = "git+https://github.com/launchbadge/sqlx#72c4e040bccfb0689529fa574124e35947c18236"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"dotenv",
|
||||
"futures 0.3.5",
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sqlx-core",
|
||||
"syn",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-rt"
|
||||
version = "0.1.0-pre"
|
||||
source = "git+https://github.com/launchbadge/sqlx#72c4e040bccfb0689529fa574124e35947c18236"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
"native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.1.1"
|
||||
@@ -3049,6 +3218,16 @@ dependencies = [
|
||||
"generic-array 0.14.2",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -3250,7 +3429,7 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 0.4.12",
|
||||
"futures 0.1.29",
|
||||
"log",
|
||||
]
|
||||
@@ -3369,9 +3548,9 @@ checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
@@ -3543,13 +3722,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-sys-stjepang"
|
||||
version = "1.0.2"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "035f8ab1fcf98d41f8fd5206a97c335604162e5eb0c25c4839062093150ddc79"
|
||||
checksum = "6fd319e971980166b53e17b1026812ad66c6b54063be879eb182342b55284694"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18bb55566f6f3f8440914233cf63df1eb60b801b5007d376cc46212cb8a9287c"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.4.2"
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.40.0"
|
||||
version = "1.39.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[profile.release]
|
||||
#lto = true
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
@@ -36,9 +36,6 @@ indexmap = "1.3.0"
|
||||
kamadak-exif = "0.5"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.23", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.16.0"
|
||||
r2d2 = "0.8.5"
|
||||
strum = "0.18.0"
|
||||
strum_macros = "0.18.0"
|
||||
backtrace = "0.3.33"
|
||||
@@ -60,20 +57,22 @@ anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
async-std-resolver = "0.19.5"
|
||||
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std", "sqlite", "macros"] }
|
||||
libsqlite3-sys = { version = "0.18", features = ["bundled", "min_sqlite_version_3_7_16"] }
|
||||
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
log = { version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
pretty_assertions = "0.6.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
|
||||
smol = "0.1.10"
|
||||
smol = "0.1.11"
|
||||
log = "0.4.8"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -98,4 +97,3 @@ internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
|
||||
@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
ADD deps/build_python.sh /builder/build_python.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||
|
||||
# Install Rust
|
||||
# Install Rust nightly
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.45.0-x86_64-unknown-linux-gnu -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf /root/.rustup/toolchains/1.45.0-x86_64-unknown-linux-gnu/share
|
||||
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.40.0"
|
||||
version = "1.39.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -19,10 +19,10 @@ fn main() {
|
||||
include_str!("deltachat.pc.in"),
|
||||
name = "deltachat",
|
||||
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or("".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -318,11 +318,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `basic_web_rtc_instance` = address to webrtc signaling server (https://github.com/cracker0dks/basicwebrtc)
|
||||
* that should be used for opening video hangouts.
|
||||
* This property is only used in the UIs not by the core itself.
|
||||
* Format: https://example.com/subdir
|
||||
* The other properties that are needed for a call such as the roomname will be set by the client in the anchor part of the url.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -982,7 +977,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just
|
||||
* before the given ID in the returned array. Set this to 0 if you do not want this behaviour.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
@@ -1177,16 +1171,6 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Get the chat's ephemeral message timer.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID.
|
||||
*
|
||||
* @return ephemeral timer value in seconds, 0 if the timer is disabled or if there is an error
|
||||
*/
|
||||
uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
@@ -1319,21 +1303,6 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
int dc_set_chat_name (dc_context_t* context, uint32_t chat_id, const char* name);
|
||||
|
||||
/**
|
||||
* Set the chat's ephemeral message timer.
|
||||
*
|
||||
* This timer is applied to all messages in a chat and starts when the
|
||||
* message is read. The setting is synchronized to all clients
|
||||
* participating in a chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID to set the ephemeral message timer for.
|
||||
* @param timer The timer value in seconds or 0 to disable the timer.
|
||||
*
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
@@ -2316,6 +2285,17 @@ int dc_array_is_independent (const dc_array_t* array, size_t in
|
||||
int dc_array_search_id (const dc_array_t* array, uint32_t needle, size_t* ret_index);
|
||||
|
||||
|
||||
/**
|
||||
* Get raw pointer to the data.
|
||||
*
|
||||
* @memberof dc_array_t
|
||||
* @param array The array object.
|
||||
* @return Raw pointer to the array. You MUST NOT free the data. You MUST NOT access the data beyond the current item count.
|
||||
* It is not possible to enlarge the array this way. Calling any other dc_array*()-function may discard the returned pointer.
|
||||
*/
|
||||
const uint32_t* dc_array_get_raw (const dc_array_t* array);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_chatlist_t
|
||||
*
|
||||
@@ -3031,32 +3011,6 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get ephemeral timer duration for message.
|
||||
*
|
||||
* To check if the timer is started and calculate remaining time,
|
||||
* use dc_msg_get_ephemeral_timestamp().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Duration in seconds, or 0 if no timer is set.
|
||||
*/
|
||||
uint32_t dc_msg_get_ephemeral_timer (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Get timestamp of ephemeral message removal.
|
||||
*
|
||||
* If returned value is non-zero, you can calculate the * fraction of
|
||||
* time remaining by divinding the difference between the current timestamp
|
||||
* and this timestamp by dc_msg_get_ephemeral_timer().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Time of message removal, 0 if the timer is not started.
|
||||
*/
|
||||
int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary for a message.
|
||||
*
|
||||
@@ -4225,11 +4179,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHAT_MODIFIED 2020
|
||||
|
||||
/**
|
||||
* Chat ephemeral timer changed.
|
||||
*/
|
||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
@@ -4512,15 +4461,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
#define DC_STR_FAILED_SENDING_TO 74
|
||||
#define DC_STR_EPHEMERAL_DISABLED 75
|
||||
#define DC_STR_EPHEMERAL_SECONDS 76
|
||||
#define DC_STR_EPHEMERAL_MINUTE 77
|
||||
#define DC_STR_EPHEMERAL_HOUR 78
|
||||
#define DC_STR_EPHEMERAL_DAY 79
|
||||
#define DC_STR_EPHEMERAL_WEEK 80
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
|
||||
#define DC_STR_COUNT 81
|
||||
#define DC_STR_COUNT 74
|
||||
|
||||
/*
|
||||
* @}
|
||||
|
||||
@@ -1,56 +1,46 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
|
||||
/* * the structure behind dc_array_t */
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum dc_array_t {
|
||||
MsgIds(Vec<MsgId>),
|
||||
Chat(Vec<ChatItem>),
|
||||
Locations(Vec<Location>),
|
||||
Uint(Vec<u32>),
|
||||
}
|
||||
|
||||
impl dc_array_t {
|
||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
dc_array_t::Uint(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Constructs a new, empty `dc_array_t` holding locations with specified `capacity`.
|
||||
pub fn new_locations(capacity: usize) -> Self {
|
||||
dc_array_t::Locations(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
pub fn add_id(&mut self, item: u32) {
|
||||
if let Self::Uint(array) = self {
|
||||
array.push(item);
|
||||
} else {
|
||||
panic!("Attempt to add id to array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_location(&mut self, location: Location) {
|
||||
if let Self::Locations(array) = self {
|
||||
array.push(location)
|
||||
} else {
|
||||
panic!("Attempt to add a location to array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id(&self, index: usize) -> u32 {
|
||||
match self {
|
||||
Self::MsgIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
Self::Uint(array) => array[index],
|
||||
Self::Uint(array) => array[index] as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||
ChatItem::Message { .. } => None,
|
||||
ChatItem::Marker1 { .. } => None,
|
||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||
}),
|
||||
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
|
||||
Self::Uint(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(_) => None,
|
||||
Self::Locations(array) => array
|
||||
.get(index)
|
||||
.and_then(|location| location.marker.as_deref()),
|
||||
Self::Uint(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_location(&self, index: usize) -> &Location {
|
||||
pub fn get_location(&self, index: usize) -> &Location {
|
||||
if let Self::Locations(array) = self {
|
||||
&array[index]
|
||||
} else {
|
||||
@@ -58,18 +48,55 @@ impl dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the array.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Locations(array) => array.is_empty(),
|
||||
Self::Uint(array) => array.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the array.
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::MsgIds(array) => array.len(),
|
||||
Self::Chat(array) => array.len(),
|
||||
Self::Locations(array) => array.len(),
|
||||
Self::Uint(array) => array.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
|
||||
(0..self.len()).find(|i| self.get_id(*i) == needle)
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
Self::Locations(array) => array.clear(),
|
||||
Self::Uint(array) => array.clear(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_id(&self, needle: u32) -> Option<usize> {
|
||||
if let Self::Uint(array) = self {
|
||||
for (i, &u) in array.iter().enumerate() {
|
||||
if u == needle {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
panic!("Attempt to search for id in array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_ids(&mut self) {
|
||||
if let dc_array_t::Uint(v) = self {
|
||||
v.sort();
|
||||
} else {
|
||||
panic!("Attempt to sort array of something other than uints");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *const u32 {
|
||||
if let dc_array_t::Uint(v) = self {
|
||||
v.as_ptr()
|
||||
} else {
|
||||
panic!("Attempt to convert array of something other than uints to raw");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +106,6 @@ impl From<Vec<u32>> for dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<MsgId>> for dc_array_t {
|
||||
fn from(array: Vec<MsgId>) -> Self {
|
||||
dc_array_t::MsgIds(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ChatItem>> for dc_array_t {
|
||||
fn from(array: Vec<ChatItem>) -> Self {
|
||||
dc_array_t::Chat(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Location>> for dc_array_t {
|
||||
fn from(array: Vec<Location>) -> Self {
|
||||
dc_array_t::Locations(array)
|
||||
@@ -103,11 +118,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dc_array() {
|
||||
let arr: dc_array_t = Vec::<u32>::new().into();
|
||||
assert!(arr.len() == 0);
|
||||
let mut arr = dc_array_t::new(7);
|
||||
assert!(arr.is_empty());
|
||||
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
|
||||
assert_eq!(arr.len(), 1000);
|
||||
|
||||
@@ -115,15 +131,31 @@ mod tests {
|
||||
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
||||
}
|
||||
|
||||
assert_eq!(arr.search_id(10), Some(8));
|
||||
assert_eq!(arr.search_id(1), None);
|
||||
arr.clear();
|
||||
|
||||
assert!(arr.is_empty());
|
||||
|
||||
arr.add_id(13);
|
||||
arr.add_id(7);
|
||||
arr.add_id(666);
|
||||
arr.add_id(0);
|
||||
arr.add_id(5000);
|
||||
|
||||
arr.sort_ids();
|
||||
|
||||
assert_eq!(arr.get_id(0), 0);
|
||||
assert_eq!(arr.get_id(1), 7);
|
||||
assert_eq!(arr.get_id(2), 13);
|
||||
assert_eq!(arr.get_id(3), 666);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_dc_array_out_of_bounds() {
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
let mut arr = dc_array_t::new(7);
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
arr.get_id(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![deny(clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -28,7 +27,6 @@ use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock::StockMessage;
|
||||
@@ -351,8 +349,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::MsgDelivered { chat_id, .. }
|
||||
| Event::MsgFailed { chat_id, .. }
|
||||
| Event::MsgRead { chat_id, .. }
|
||||
| Event::ChatModified(chat_id)
|
||||
| Event::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
| Event::ChatModified(chat_id) => chat_id.to_u32().unwrap_or_default() as libc::c_int,
|
||||
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
@@ -399,10 +396,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::IncomingMsg { msg_id, .. }
|
||||
| Event::MsgDelivered { msg_id, .. }
|
||||
| Event::MsgFailed { msg_id, .. }
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32().unwrap_or_default() as libc::c_int,
|
||||
Event::SecurejoinInviterProgress { progress, .. }
|
||||
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
Event::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +439,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| Event::ConfigureProgress(_)
|
||||
| Event::ImexProgress(_)
|
||||
| Event::SecurejoinInviterProgress { .. }
|
||||
| Event::SecurejoinJoinerProgress { .. }
|
||||
| Event::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
| Event::SecurejoinJoinerProgress { .. } => ptr::null_mut(),
|
||||
Event::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
@@ -487,7 +482,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
events
|
||||
.recv_sync()
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
.unwrap_or_else(|| ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -835,11 +830,15 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
.await
|
||||
.into(),
|
||||
))
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_msgs")
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -900,7 +899,7 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.get_fresh_msgs()
|
||||
.await
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
@@ -968,7 +967,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_media(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
@@ -977,8 +976,12 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.into(),
|
||||
))
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_media")
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1018,8 +1021,9 @@ pub unsafe extern "C" fn dc_get_next_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_next_media")
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or(0)
|
||||
.unwrap_or_else(|| 0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1085,7 +1089,10 @@ 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 list = chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_contacts");
|
||||
let arr = dc_array_t::from(list);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
@@ -1106,7 +1113,7 @@ pub unsafe extern "C" fn dc_search_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query))
|
||||
.await
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
@@ -1284,49 +1291,6 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
// Timer value 0 is returned in the rare case of a database error,
|
||||
// but it is not dangerous since it is only meant to be used as a
|
||||
// default when changing the value. Such errors should not be
|
||||
// ignored when ephemeral timer value is used to construct
|
||||
// message headers.
|
||||
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
|
||||
.log_err(ctx, "Failed to get ephemeral timer")
|
||||
.unwrap_or_default()
|
||||
.to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
|
||||
.await
|
||||
.log_err(ctx, "Failed to set ephemeral timer")
|
||||
.is_ok() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_info(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1338,7 +1302,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 get_msg_info")
|
||||
})
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1897,7 +1866,11 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(&ctx, latitude, longitude, accuracy)) as _
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed location::set")
|
||||
}) as _
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2022,7 +1995,7 @@ pub unsafe extern "C" fn dc_array_get_timestamp(
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*array).get_timestamp(index).unwrap_or_default()
|
||||
(*array).get_location(index).timestamp
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_chat_id(
|
||||
@@ -2069,7 +2042,7 @@ pub unsafe extern "C" fn dc_array_get_marker(
|
||||
return std::ptr::null_mut(); // NULL explicitly defined as "no markers"
|
||||
}
|
||||
|
||||
if let Some(s) = (*array).get_marker(index) {
|
||||
if let Some(s) = &(*array).get_location(index).marker {
|
||||
s.strdup()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
@@ -2097,6 +2070,16 @@ pub unsafe extern "C" fn dc_array_search_id(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_raw(array: *const dc_array_t) -> *const u32 {
|
||||
if array.is_null() {
|
||||
eprintln!("ignoring careless call to dc_array_get_raw()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
(*array).as_ptr()
|
||||
}
|
||||
|
||||
// Return the independent-state of the location at the given index.
|
||||
// Independent locations do not belong to the track of the user.
|
||||
// Returns 1 if location belongs to the track of the user,
|
||||
@@ -2287,7 +2270,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(&ctx).await {
|
||||
match ffi_chat
|
||||
.chat
|
||||
.get_profile_image(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_profile_image")
|
||||
{
|
||||
Some(p) => p.to_string_lossy().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -2303,7 +2291,13 @@ 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(async move {
|
||||
ffi_chat
|
||||
.chat
|
||||
.get_color(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed dc_chat_get_color")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2676,26 +2670,6 @@ pub unsafe extern "C" fn dc_msg_get_showpadlock(msg: *mut dc_msg_t) -> libc::c_i
|
||||
ffi_msg.message.get_showpadlock() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timer(msg: *mut dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timer()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
msg: *mut dc_msg_t,
|
||||
|
||||
@@ -105,9 +105,8 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,9 +122,8 @@ fn os_str_to_c_string_unicode(
|
||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
||||
) -> Result<CString, CStringError> {
|
||||
match os_str.as_ref().to_str() {
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
}),
|
||||
None => Err(CStringError::NotUnicode),
|
||||
}
|
||||
|
||||
@@ -8,36 +8,33 @@ use quote::quote;
|
||||
// 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 {
|
||||
#[proc_macro_derive(Sqlx)]
|
||||
pub fn sqlx_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
let value = rusqlite::types::Value::Integer(num);
|
||||
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
||||
std::result::Result::Ok(output)
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
||||
impl<'q> sqlx::encode::Encode<'q, sqlx::sqlite::Sqlite> for #name {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>) -> sqlx::encode::IsNull {
|
||||
num_traits::ToPrimitive::to_i32(self).expect("invalid type").encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'de> sqlx::decode::Decode<'de, sqlx::sqlite::Sqlite> for #name {
|
||||
fn decode(value: sqlx::sqlite::SqliteValueRef) -> std::result::Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
|
||||
let raw: i32 = sqlx::decode::Decode::decode(value)?;
|
||||
|
||||
Ok(num_traits::FromPrimitive::from_i32(raw).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl sqlx::types::Type<sqlx::sqlite::Sqlite> for #name {
|
||||
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
||||
<i32 as sqlx::types::Type<_>>::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
simplify/streamline mark-seen/delete/move/send-mdn job handling
|
||||
---------------------------------------------------------------
|
||||
|
||||
Idea: Introduce a new "msgwork" sql table that looks very
|
||||
much like the jobs table but has a primary key "msgid"
|
||||
and no job id and no foreign-id anymore. This opens up
|
||||
bulk-processing by looking at the whole table and combining
|
||||
flag-setting to reduce imap-roundtrips and select-folder calls.
|
||||
|
||||
Concretely, these IMAP jobs:
|
||||
|
||||
DeleteMsgOnImap
|
||||
MarkseenMsgOnImap
|
||||
MoveMsg
|
||||
|
||||
Would be replaced by a few per-message columns in the new msgwork table:
|
||||
|
||||
- needs_mark_seen: (bool) message shall be marked as seen on imap
|
||||
- needs_to_move: (bool) message should be moved to mvbox_folder
|
||||
- deletion_time: (target_time or 0) message shall be deleted at specified time
|
||||
- needs_send_mdn: (bool) MDN shall be sent
|
||||
|
||||
The various places that currently add the (replaced) jobs
|
||||
would now add/modify the respective message record in the message-work table.
|
||||
|
||||
Looking at a single message-work entry conceptually looks like this::
|
||||
|
||||
if msg.server_uid==0:
|
||||
return RetryLater # nothing can be done without server_uid
|
||||
|
||||
if msg.deletion_time > current_time:
|
||||
imap.mark_delete(msg) # might trigger early exit with a RetryLater/Failed
|
||||
clear(needs_deletion)
|
||||
clear(mark_seen)
|
||||
|
||||
if needs_mark_seen:
|
||||
imap.mark_seen(msg) # might trigger early exit with a RetryLater/Failed
|
||||
clear(needs_mark_seen)
|
||||
|
||||
if needs_send_mdn:
|
||||
schedule_smtp_send_mdn(msg)
|
||||
clear(needs_send_mdn)
|
||||
|
||||
if any_flag_set():
|
||||
retrylater
|
||||
# remove msgwork entry from table
|
||||
|
||||
|
||||
Notes/Questions:
|
||||
|
||||
- it's unclear how much we need per-message retry-time tracking/backoff
|
||||
|
||||
- drafting bulk processing algo is useful before
|
||||
going for the implementation, i.e. including select_folder calls etc.
|
||||
|
||||
- maybe it's better to not have bools for the flags but
|
||||
|
||||
0 (no change)
|
||||
1 (set the imap flag)
|
||||
2 (clear the imap flag)
|
||||
|
||||
and design such that we can cover all imap flags.
|
||||
|
||||
- It might not be neccessary to keep needs_send_mdn state in this table
|
||||
if this can be decided rather when we succeed with mark_seen/mark_delete.
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -28,7 +28,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", paramsv![])
|
||||
.execute("DELETE FROM jobs;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
@@ -36,7 +36,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||
.execute("DELETE FROM acpeerstates;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
@@ -44,7 +44,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", paramsv![])
|
||||
.execute("DELETE FROM keypairs;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
@@ -52,35 +52,35 @@ 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;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||
.execute("DELETE FROM chats_contacts;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
paramsv![],
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
.execute("DELETE FROM leftgrps;", paramsx![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
@@ -118,7 +118,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config(context, "import_spec").await;
|
||||
let rs = context.sql().get_raw_config("import_spec").await;
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
return false;
|
||||
@@ -215,7 +215,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
if msg_id.is_daymarker() {
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
@@ -276,7 +276,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr).await;
|
||||
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
|
||||
if peerstate.is_ok() && contact_id != 1 as libc::c_uint {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
@@ -484,7 +484,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
context.maybe_network().await;
|
||||
}
|
||||
"housekeeping" => {
|
||||
sql::housekeeping(&context).await;
|
||||
sql::housekeeping(&context).await?;
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
@@ -573,17 +573,8 @@ 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: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
@@ -603,7 +594,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(),
|
||||
@@ -700,7 +691,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;
|
||||
@@ -771,7 +762,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
@@ -867,7 +858,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 {
|
||||
@@ -901,7 +892,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await;
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"listfresh" => {
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
1.40.1
|
||||
0.900.0 (DRAFT)
|
||||
---------------
|
||||
|
||||
- emit "ac_member_removed" event (with 'actor' being the removed contact)
|
||||
for when a user leaves a group.
|
||||
|
||||
- fix create_contact(addr) when addr is the self-contact.
|
||||
|
||||
|
||||
1.40.0
|
||||
---------------
|
||||
|
||||
- uses latest 1.40+ Delta Chat core
|
||||
|
||||
- refactored internals to use plugin-approach
|
||||
|
||||
- introduced PerAccount and Global hooks that plugins can implement
|
||||
@@ -21,7 +10,6 @@
|
||||
- introduced two documented examples for an echo and a group-membership
|
||||
tracking plugin.
|
||||
|
||||
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
|
||||
Installing bindings from source (Updated: 20-Jan-2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first. Deltachat needs a specific nightly
|
||||
version, the easiest is probably to first install Rust stable from
|
||||
rustup and then use this to install the correct nightly version.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then GIT clone the deltachat-core-rust repo and get the actual
|
||||
rust- and cargo-toolchain needed by deltachat::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
rustup show
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
./install_python_bindings.py
|
||||
|
||||
The installation might take a while, depending on your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run "liveconfig" functional tests you can set
|
||||
``DCC_NEW_TMP_EMAIL`` to:
|
||||
|
||||
- a particular https-url that you can ask for from the delta
|
||||
chat devs. This is implemented on the server side via
|
||||
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
|
||||
|
||||
- or the path of a file that contains two lines, each describing
|
||||
via "addr=... mail_pw=..." a test account login that will
|
||||
be used for the live tests.
|
||||
|
||||
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
|
||||
e-mail accounts and run through all functional "liveconfig" tests.
|
||||
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
========================================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
|
||||
yourself <sourceinstall>`_.
|
||||
without any "build-from-source" steps.
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
|
||||
virtualenv venv # or: python -m venv
|
||||
@@ -41,78 +103,6 @@ To verify it worked::
|
||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
This will run all "offline" tests and skip all functional
|
||||
end-to-end tests that require accounts on real e-mail servers.
|
||||
|
||||
.. _livetests:
|
||||
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
|
||||
|
||||
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
|
||||
These accounts exists for one 1hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
pytest
|
||||
|
||||
# or if you have installed pytest-xdist for parallel test execution
|
||||
pytest -n6
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source (Updated: July 2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
python install_python_bindings.py
|
||||
|
||||
The core compilation and bindings building might take a while,
|
||||
depending on the speed of your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
|
||||
Code examples
|
||||
=============
|
||||
|
||||
|
||||
3
python/doc/_templates/globaltoc.html
vendored
3
python/doc/_templates/globaltoc.html
vendored
@@ -9,7 +9,8 @@
|
||||
</ul>
|
||||
<b>external links:</b>
|
||||
<ul>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core">github repository</a></li>
|
||||
<!-- <li><a href="https://lists.codespeak.net/postorius/lists/muacrypt.lists.codespeak.net">Mailing list</></li> <-->
|
||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -32,16 +32,16 @@ class GroupTrackingPlugin:
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -69,11 +69,11 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
*ac_member_added {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
*ac_member_removed {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
@@ -246,6 +246,7 @@ class Account(object):
|
||||
addr = as_dc_charpointer(addr)
|
||||
name = as_dc_charpointer(name)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
@@ -606,24 +607,12 @@ class Account(object):
|
||||
self.stop_io()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
|
||||
# if _dc_context is unref'ed the event thread should quickly
|
||||
# receive the termination signal. However, some python code might
|
||||
# still hold a reference and so we use a secondary signal
|
||||
# to make sure the even thread terminates if it receives any new
|
||||
# event, indepedently from waiting for the core to send NULL to
|
||||
# get_next_event().
|
||||
self._event_thread.mark_shutdown()
|
||||
# the dc_context_unref triggers get_next_event to return ffi.NULL
|
||||
# which in turns makes the event thread finish execution
|
||||
self._dc_context = None
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
try:
|
||||
self._event_thread.wait(timeout=2)
|
||||
except RuntimeError as e:
|
||||
self.log("Waiting for event thread failed: {}".format(e))
|
||||
|
||||
if self._event_thread.is_alive():
|
||||
self.log("WARN: event thread did not terminate yet, ignoring.")
|
||||
self._event_thread.wait()
|
||||
|
||||
self._shutdown_event.set()
|
||||
|
||||
|
||||
@@ -139,22 +139,6 @@ class Chat(object):
|
||||
"""
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
|
||||
def get_type(self):
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
|
||||
@@ -159,13 +159,9 @@ class DirectImap:
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
requested = [b'BODY.PEEK[]', FLAGS]
|
||||
requested = [b'BODY.PEEK[HEADER]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[]']
|
||||
if not body_bytes:
|
||||
log("Message", uid, "has empty body")
|
||||
continue
|
||||
|
||||
body_bytes = data[b'BODY[HEADER]']
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -196,7 +192,6 @@ class DirectImap:
|
||||
raise TimeoutError
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def idle_wait_for_seen(self):
|
||||
|
||||
@@ -86,11 +86,11 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
rex = re.compile(regex)
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.search(ev.data2):
|
||||
if rex.match(ev.data2):
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
@@ -139,7 +139,6 @@ class EventThread(threading.Thread):
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@contextmanager
|
||||
@@ -148,15 +147,12 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self):
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
""" get and run events until shutdown. """
|
||||
@@ -168,12 +164,10 @@ class EventThread(threading.Thread):
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
while 1:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL:
|
||||
break
|
||||
if self._marked_for_shutdown:
|
||||
break
|
||||
evt = lib.dc_event_get_id(event)
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
# the following code relates to the deltachat/_build.py's helper
|
||||
|
||||
@@ -16,7 +16,7 @@ class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
All hooks are executed in a dedicated Event thread.
|
||||
Hooks are generally not allowed to block/last long as this
|
||||
Hooks are not allowed to block/last long as this
|
||||
blocks overall event processing on the python side.
|
||||
"""
|
||||
@classmethod
|
||||
@@ -31,6 +31,10 @@ class PerAccount:
|
||||
|
||||
ffi_event has "name", "data1", "data2" values as specified
|
||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||
|
||||
DANGER: this hook is executed from the callback invoked by core.
|
||||
Hook implementations need to be short running and can typically
|
||||
not call back into core because this would easily cause recursion issues.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
@@ -51,37 +55,19 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
""" Called when an outgoing message has been delivered to SMTP.
|
||||
|
||||
:param message: Message that was just delivered.
|
||||
"""
|
||||
""" Called when an outgoing message has been delivered to SMTP. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
""" Chat was created or modified regarding membership, avatar, title.
|
||||
|
||||
:param chat: Chat which was modified.
|
||||
"""
|
||||
""" Chat was created or modified regarding membership, avatar, title. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
""" Called for each contact added to an accepted chat.
|
||||
|
||||
:param chat: Chat where contact was added.
|
||||
:param contact: Contact that was added.
|
||||
:param actor: Who added the contact (None if it was our self-addr)
|
||||
:param message: The original system message that reports the addition.
|
||||
"""
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
""" Called for each contact added to an accepted chat. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
""" Called for each contact removed from a chat.
|
||||
|
||||
:param chat: Chat where contact was removed.
|
||||
:param contact: Contact that was removed.
|
||||
:param actor: Who removed the contact (None if it was our self-addr)
|
||||
:param message: The original system message that reports the removal.
|
||||
"""
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
""" Called for each contact removed from a chat. """
|
||||
|
||||
|
||||
class Global:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
""" The Message object. """
|
||||
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
@@ -155,26 +154,6 @@ class Message(object):
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
"""Ephemeral timer in seconds
|
||||
|
||||
:returns: timer in seconds or None if there is no timer
|
||||
"""
|
||||
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
|
||||
if timer:
|
||||
return timer
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timestamp(self):
|
||||
"""UTC time when the message will be deleted.
|
||||
|
||||
:returns: naive datetime.datetime() object or None if the timer is not started.
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -357,43 +336,20 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
if actor == "me":
|
||||
actor = None
|
||||
else:
|
||||
actor = msg.account.get_contact_by_addr(actor)
|
||||
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def extract_addr(text):
|
||||
m = re.match(r'.*\((.+@.+)\)', text)
|
||||
if m:
|
||||
text = m.group(1)
|
||||
text = text.rstrip(".")
|
||||
return text.strip()
|
||||
if res:
|
||||
contact = msg.account.get_contact_by_addr(res[1])
|
||||
if contact:
|
||||
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
""" return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple """
|
||||
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
# Member x@y removed by a@b
|
||||
text = text.lower()
|
||||
m = re.match(r'member (.+) (removed|added) by (.+)', text)
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
return "removed", addr, addr
|
||||
parts = text.split()
|
||||
if parts[0] == "member":
|
||||
if parts[2] in ("removed", "added"):
|
||||
return parts[2], parts[1]
|
||||
if parts[3] in ("removed", "added"):
|
||||
return parts[3], parts[2].strip("()")
|
||||
|
||||
@@ -152,7 +152,7 @@ class SessionLiveConfigFromURL:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
@@ -231,13 +231,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
logger = FFIEventLogger(ac)
|
||||
logger.init_time = self.init_time
|
||||
ac.add_account_plugin(logger)
|
||||
ac.add_account_plugin(FFIEventLogger(ac))
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
@@ -247,7 +244,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
@@ -290,6 +290,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
@@ -330,6 +332,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
|
||||
@@ -11,21 +11,8 @@ from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.", ("removed", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org")),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
@@ -129,11 +116,6 @@ class TestOfflineContact:
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
@@ -470,12 +452,12 @@ class TestOfflineChat:
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
def ac_member_added(self, chat, contact):
|
||||
in_list.append(("added", chat, contact))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
def ac_member_removed(self, chat, contact):
|
||||
in_list.append(("removed", chat, contact))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
@@ -504,11 +486,10 @@ class TestOfflineChat:
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
for in_cmd, in_chat, in_contact in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
@@ -536,7 +517,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
imap2.idle_start()
|
||||
ac2.direct_imap.idle_start()
|
||||
chat12.send_text("hello")
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -1561,97 +1542,6 @@ class TestOnlineAccount:
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
imap2.idle_start()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
sent_msg = chat1.send_text("hello")
|
||||
imap2.idle_check(terminate=False)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
imap2.idle_check(terminate=True)
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
|
||||
assert len(imap2.get_all_messages()) == 0
|
||||
|
||||
# Mark deleted message as seen and check that read receipt arrives
|
||||
msg.mark_seen()
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 == chat1.id
|
||||
assert ev.data2 == sent_msg.id
|
||||
|
||||
def test_ephemeral_timer(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: set ephemeral timer to 60")
|
||||
chat1.set_ephemeral_timer(60)
|
||||
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert chat2.get_ephemeral_timer() == 60
|
||||
assert system_message1.is_system_message()
|
||||
|
||||
# Disabled until markers are implemented
|
||||
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
|
||||
|
||||
lp.sec("ac2: send message to ac1")
|
||||
sent_message = chat2.send_text("message")
|
||||
assert sent_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
|
||||
|
||||
# Timer is started immediately for sent messages
|
||||
assert sent_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in sent_message.get_message_info()
|
||||
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
text_message = ac1._evtracker.wait_next_incoming_message()
|
||||
assert text_message.text == "message"
|
||||
assert text_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
|
||||
|
||||
# Timer should not start until message is displayed
|
||||
assert text_message.ephemeral_timestamp is None
|
||||
assert "Expires: " not in text_message.get_message_info()
|
||||
text_message.mark_seen()
|
||||
text_message = ac1.get_message_by_id(text_message.id)
|
||||
assert text_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in text_message.get_message_info()
|
||||
|
||||
lp.sec("ac2: set ephemeral timer to 0")
|
||||
chat2.set_ephemeral_timer(0)
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
lp.sec("ac1: receive system message about ephemeral timer modification")
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message2 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert system_message2.ephemeral_timer is None
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.45.0
|
||||
1.43.1
|
||||
|
||||
@@ -5,29 +5,19 @@ import sys
|
||||
import re
|
||||
import pathlib
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
rex = re.compile(r'version = "(\S+)"')
|
||||
|
||||
|
||||
def regex_matches(relpath, regex=rex):
|
||||
def read_toml_version(relpath):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
for line in open(str(p)):
|
||||
m = regex.match(line)
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
return m
|
||||
|
||||
|
||||
def read_toml_version(relpath):
|
||||
res = regex_matches(relpath, rex)
|
||||
if res is not None:
|
||||
return res.group(1)
|
||||
return m.group(1)
|
||||
raise ValueError("no version found in {}".format(relpath))
|
||||
|
||||
|
||||
def replace_toml_version_and_lto(relpath, newversion):
|
||||
lto_rex = re.compile(r'#?\s*lto =.*')
|
||||
def replace_toml_version(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
tmp_path = str(p) + "_tmp"
|
||||
@@ -35,32 +25,18 @@ def replace_toml_version_and_lto(relpath, newversion):
|
||||
for line in open(str(p)):
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
print("{}: set version={}".format(relpath, newversion))
|
||||
f.write('version = "{}"\n'.format(newversion))
|
||||
else:
|
||||
m = lto_rex.match(line)
|
||||
if m:
|
||||
print("{}: setting lto = true".format(relpath))
|
||||
line = "lto = true\n"
|
||||
f.write(line)
|
||||
os.rename(tmp_path, str(p))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
if len(sys.argv) < 2:
|
||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
newversion = opts.newversion
|
||||
newversion = sys.argv[1]
|
||||
if newversion.count(".") < 2:
|
||||
raise SystemExit("need at least two dots in version")
|
||||
|
||||
@@ -76,13 +52,10 @@ def main():
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
|
||||
replace_toml_version_and_lto("Cargo.toml", newversion)
|
||||
replace_toml_version_and_lto("deltachat-ffi/Cargo.toml", newversion)
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
print("adding changes to git index")
|
||||
subprocess.call(["git", "add", "-u"])
|
||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||
|
||||
@@ -90,8 +63,3 @@ def main():
|
||||
print("")
|
||||
print(" git tag {}".format(newversion))
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use deltachat_derive::*;
|
||||
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
/// Possible values for encryption preference
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[repr(u8)]
|
||||
pub enum EncryptPreference {
|
||||
NoPreference = 0,
|
||||
|
||||
@@ -168,9 +168,6 @@ impl<'a> BlobObject<'a> {
|
||||
/// subdirectory is used and [BlobObject::sanitise_name] does not
|
||||
/// modify the filename.
|
||||
///
|
||||
/// Paths into the blob directory may be either defined by an absolute path
|
||||
/// or by the relative prefix `$BLOBDIR`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This merely delegates to the [BlobObject::create_and_copy] and
|
||||
@@ -182,11 +179,6 @@ impl<'a> BlobObject<'a> {
|
||||
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
||||
BlobObject::from_path(context, src)
|
||||
} else if src.as_ref().starts_with("$BLOBDIR/") {
|
||||
BlobObject::from_name(
|
||||
context,
|
||||
src.as_ref().to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
} else {
|
||||
BlobObject::create_and_copy(context, src).await
|
||||
}
|
||||
|
||||
1141
src/chat.rs
1141
src/chat.rs
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
@@ -77,7 +76,7 @@ impl Chatlist {
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -100,23 +99,12 @@ impl Chatlist {
|
||||
|
||||
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||
// messages get deleted to avoid reloading the same chatlist.
|
||||
if let Err(err) = delete_expired_messages(context).await {
|
||||
if let Err(err) = delete_device_expired_messages(context).await {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
@@ -143,7 +131,7 @@ impl Chatlist {
|
||||
// shown at all permanent in the chatlist.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
context.sql.query_rows(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -152,16 +140,14 @@ impl Chatlist {
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
||||
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,
|
||||
ORDER BY c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsx![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
// show archived chats
|
||||
@@ -170,7 +156,7 @@ impl Chatlist {
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_rows(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -186,9 +172,7 @@ 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,
|
||||
paramsx![MessageState::OutDraft],
|
||||
)
|
||||
.await?
|
||||
} else if let Some(query) = query {
|
||||
@@ -204,7 +188,7 @@ impl Chatlist {
|
||||
let str_like_cmd = format!("%{}%", query);
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_rows(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -220,9 +204,7 @@ 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,
|
||||
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -235,7 +217,7 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
let mut ids = context.sql.query_rows(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -251,10 +233,9 @@ impl Chatlist {
|
||||
AND NOT c.archived=?3
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
).await?;
|
||||
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||
{
|
||||
@@ -388,15 +369,15 @@ impl Chatlist {
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
context
|
||||
let v: i32 = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsv![],
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
v as u32
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
@@ -404,21 +385,21 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
paramsv![],
|
||||
.query_value(
|
||||
r#"
|
||||
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;
|
||||
"#,
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -117,36 +117,24 @@ pub enum Config {
|
||||
|
||||
#[strum(serialize = "sys.config_keys")]
|
||||
SysConfigKeys,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
/// Whether we send a warning if the password is wrong (set to false when we send a warning
|
||||
/// because we do not want to send a second warning)
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// address to webrtc signaling server (https://github.com/cracker0dks/basicwebrtc)
|
||||
/// that should be used for opening video hangouts.
|
||||
/// This property is only used in the UIs not by the core itself.
|
||||
/// Format: https://example.com/subdir
|
||||
/// The other properties that are needed for a call such as the roomname will be set by the client in the anchor part of the url.
|
||||
BasicWebRTCInstance,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> bool {
|
||||
self.sql.get_raw_config(self, key).await.is_some()
|
||||
self.sql.get_raw_config(key).await.is_some()
|
||||
}
|
||||
|
||||
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
|
||||
pub async fn get_config(&self, key: Config) -> Option<String> {
|
||||
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() {
|
||||
@@ -201,7 +189,7 @@ impl Context {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Email accounts autoconfiguration process module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
@@ -35,7 +37,7 @@ 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
|
||||
self.sql.get_raw_config_bool("configured").await
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
@@ -70,7 +72,6 @@ impl Context {
|
||||
|
||||
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) = provider::get_provider_info(¶m.addr) {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
@@ -101,12 +102,11 @@ impl Context {
|
||||
|
||||
match success {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!(self, "Configure Failed: {}", err);
|
||||
progress!(self, 0);
|
||||
Err(err)
|
||||
}
|
||||
@@ -459,10 +459,7 @@ async fn try_imap_connections(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
|
||||
}
|
||||
|
||||
progress!(context, 670);
|
||||
@@ -496,7 +493,7 @@ async fn try_imap_connection(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
progress!(context, 650 + variation * 30);
|
||||
@@ -556,7 +553,7 @@ async fn try_smtp_connections(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("No SMTP connection");
|
||||
return Ok(());
|
||||
}
|
||||
progress!(context, 850);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # Constants
|
||||
#![allow(dead_code)]
|
||||
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -25,12 +24,11 @@ const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
#[repr(i32)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
@@ -43,8 +41,8 @@ impl Default for Blocked {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(i32)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
AcceptedContacts = 1,
|
||||
@@ -57,8 +55,8 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(i32)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
Worse = 1,
|
||||
@@ -70,7 +68,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, sqlx::Type)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
Default = 0,
|
||||
@@ -127,13 +125,12 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
IntoStaticStr,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
#[repr(i32)]
|
||||
pub enum Chattype {
|
||||
Undefined = 0,
|
||||
Single = 100,
|
||||
@@ -243,10 +240,9 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Viewtype {
|
||||
@@ -323,6 +319,54 @@ const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
||||
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
||||
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
||||
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
||||
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
||||
|
||||
// TODO: Strings need some doumentation about used placeholders.
|
||||
// These constants are used to set stock translation strings
|
||||
|
||||
const DC_STR_NOMESSAGES: usize = 1;
|
||||
const DC_STR_SELF: usize = 2;
|
||||
const DC_STR_DRAFT: usize = 3;
|
||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||
const DC_STR_DEADDROP: usize = 8;
|
||||
const DC_STR_IMAGE: usize = 9;
|
||||
const DC_STR_VIDEO: usize = 10;
|
||||
const DC_STR_AUDIO: usize = 11;
|
||||
const DC_STR_FILE: usize = 12;
|
||||
const DC_STR_STATUSLINE: usize = 13;
|
||||
const DC_STR_NEWGROUPDRAFT: usize = 14;
|
||||
const DC_STR_MSGGRPNAME: usize = 15;
|
||||
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
|
||||
const DC_STR_MSGADDMEMBER: usize = 17;
|
||||
const DC_STR_MSGDELMEMBER: usize = 18;
|
||||
const DC_STR_MSGGROUPLEFT: usize = 19;
|
||||
const DC_STR_GIF: usize = 23;
|
||||
const DC_STR_ENCRYPTEDMSG: usize = 24;
|
||||
const DC_STR_E2E_AVAILABLE: usize = 25;
|
||||
const DC_STR_ENCR_TRANSP: usize = 27;
|
||||
const DC_STR_ENCR_NONE: usize = 28;
|
||||
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
|
||||
const DC_STR_FINGERPRINTS: usize = 30;
|
||||
const DC_STR_READRCPT: usize = 31;
|
||||
const DC_STR_READRCPT_MAILBODY: usize = 32;
|
||||
const DC_STR_MSGGRPIMGDELETED: usize = 33;
|
||||
const DC_STR_E2E_PREFERRED: usize = 34;
|
||||
const DC_STR_CONTACT_VERIFIED: usize = 35;
|
||||
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
|
||||
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
|
||||
const DC_STR_ARCHIVEDCHATS: usize = 40;
|
||||
const DC_STR_STARREDMSGS: usize = 41;
|
||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||
const DC_STR_MSGACTIONBYME: usize = 63;
|
||||
const DC_STR_MSGLOCATIONENABLED: usize = 64;
|
||||
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
|
||||
const DC_STR_LOCATION: usize = 66;
|
||||
const DC_STR_STICKER: usize = 67;
|
||||
const DC_STR_COUNT: usize = 67;
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
|
||||
339
src/contact.rs
339
src/contact.rs
@@ -1,10 +1,14 @@
|
||||
//! Contacts module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::*;
|
||||
use async_std::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -70,7 +74,7 @@ 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)]
|
||||
pub enum Origin {
|
||||
@@ -131,6 +135,31 @@ impl Default for Origin {
|
||||
Origin::Unknown
|
||||
}
|
||||
}
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Contact {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
let contact = Self {
|
||||
id: u32::try_from(row.try_get::<i32, _>("id")?)
|
||||
.map_err(|err| sqlx::Error::Decode(err.into()))?,
|
||||
name: row
|
||||
.try_get::<Option<String>, _>("name")?
|
||||
.unwrap_or_default(),
|
||||
authname: row
|
||||
.try_get::<Option<String>, _>("authname")?
|
||||
.unwrap_or_default(),
|
||||
addr: row.try_get::<String, _>("addr")?,
|
||||
blocked: row
|
||||
.try_get::<Option<i32>, _>("blocked")?
|
||||
.unwrap_or_default()
|
||||
!= 0,
|
||||
origin: row.try_get("origin")?,
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
}
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
/// Contacts that are known, i. e. they came in via accepted contacts or
|
||||
@@ -161,27 +190,18 @@ pub enum VerifiedStatus {
|
||||
|
||||
impl Contact {
|
||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
||||
let mut res = context
|
||||
let mut res: Contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
|row| {
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name: row.get::<_, String>(0)?,
|
||||
authname: row.get::<_, String>(4)?,
|
||||
addr: row.get::<_, String>(1)?,
|
||||
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
|
||||
origin: row.get(2)?,
|
||||
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
},
|
||||
r#"
|
||||
SELECT id, name, addr, origin, blocked, authname, param
|
||||
FROM contacts
|
||||
WHERE id=?;
|
||||
"#,
|
||||
paramsx![contact_id as i32],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
res.name = context.stock_str(StockMessage::SelfMsg).await.to_string();
|
||||
res.addr = context
|
||||
@@ -267,8 +287,11 @@ impl Contact {
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
r#"
|
||||
UPDATE msgs SET state=?
|
||||
WHERE from_id=? AND state=?;
|
||||
"#,
|
||||
paramsx![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -303,15 +326,15 @@ impl Contact {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
return DC_CONTACT_ID_SELF;
|
||||
}
|
||||
context.sql.query_get_value(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![
|
||||
let v: i32 = context.sql.query_value(
|
||||
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE AND id>? AND origin>=? AND blocked=0;",
|
||||
paramsx![
|
||||
addr_normalized,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
min_origin as u32,
|
||||
min_origin as i32
|
||||
],
|
||||
).await.unwrap_or_default()
|
||||
).await.unwrap_or_default();
|
||||
v as u32
|
||||
}
|
||||
|
||||
/// Lookup a contact and create it if it does not exist yet.
|
||||
@@ -382,39 +405,32 @@ impl Contact {
|
||||
let mut update_authname = false;
|
||||
let mut row_id = 0;
|
||||
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
|
||||
let res: Result<(i32, String, String, Origin, String), _> = 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)?;
|
||||
paramsx![addr.to_string()],
|
||||
)
|
||||
.await;
|
||||
|
||||
if !name.as_ref().is_empty() {
|
||||
if !row_name.is_empty() {
|
||||
if (origin >= row_origin || row_name == row_authname)
|
||||
&& name.as_ref() != row_name
|
||||
{
|
||||
update_name = true;
|
||||
}
|
||||
} else {
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = res {
|
||||
if !name.as_ref().is_empty() {
|
||||
if !row_name.is_empty() {
|
||||
if (origin >= row_origin || row_name == row_authname)
|
||||
&& name.as_ref() != row_name
|
||||
{
|
||||
update_name = true;
|
||||
}
|
||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||
update_authname = true;
|
||||
}
|
||||
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
|
||||
// no name given on manual edit, this will update the name to the authname
|
||||
} else {
|
||||
update_name = true;
|
||||
}
|
||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||
update_authname = true;
|
||||
}
|
||||
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
|
||||
// no name given on manual edit, this will update the name to the authname
|
||||
update_name = true;
|
||||
}
|
||||
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
},
|
||||
)
|
||||
.await {
|
||||
row_id = id;
|
||||
row_id = id as u32;
|
||||
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
@@ -433,9 +449,13 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
paramsv![
|
||||
new_name,
|
||||
if update_addr { addr.to_string() } else { row_addr },
|
||||
paramsx![
|
||||
&new_name,
|
||||
if update_addr {
|
||||
addr.to_string()
|
||||
} else {
|
||||
row_addr
|
||||
},
|
||||
if origin > row_origin {
|
||||
origin
|
||||
} else {
|
||||
@@ -446,7 +466,7 @@ impl Contact {
|
||||
} else {
|
||||
row_authname
|
||||
},
|
||||
row_id
|
||||
row_id as i32
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -457,7 +477,7 @@ impl Contact {
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
context.sql.execute(
|
||||
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
paramsv![new_name, Chattype::Single, row_id]
|
||||
paramsx![&new_name, Chattype::Single, row_id as i32]
|
||||
).await.ok();
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
@@ -471,20 +491,17 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
||||
paramsv![
|
||||
name.as_ref().to_string(),
|
||||
addr,
|
||||
paramsx![
|
||||
name.as_ref(),
|
||||
&addr,
|
||||
origin,
|
||||
if update_authname { name.as_ref().to_string() } else { "".to_string() }
|
||||
if update_authname { name.as_ref() } else { "" }
|
||||
],
|
||||
)
|
||||
.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 {
|
||||
@@ -571,35 +588,32 @@ impl Contact {
|
||||
.map(|s| s.as_ref().to_string())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr!=?1 \
|
||||
AND c.id>?2 \
|
||||
AND c.origin>=?3 \
|
||||
AND c.blocked=0 \
|
||||
AND (c.name LIKE ?4 OR c.addr LIKE ?5) \
|
||||
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY LOWER(c.name||c.addr),c.id;",
|
||||
paramsv![
|
||||
self_addr,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT c.id FROM contacts c
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr
|
||||
WHERE c.addr!=?
|
||||
AND c.id>?
|
||||
AND c.origin>=?
|
||||
AND c.blocked=0
|
||||
AND (c.name LIKE ? OR c.addr LIKE ?)
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0)
|
||||
ORDER BY LOWER(c.name||c.addr),c.id
|
||||
"#,
|
||||
)
|
||||
.bind(&self_addr)
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL as i32)
|
||||
.bind(Origin::IncomingReplyTo)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(if flag_verified_only { 0i32 } else { 1i32 })
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(id) = rows.next().await {
|
||||
let (id,): (i32,) = id?;
|
||||
ret.push(id as u32);
|
||||
}
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -620,17 +634,15 @@ impl Contact {
|
||||
} else {
|
||||
add_self = true;
|
||||
|
||||
context.sql.query_map(
|
||||
"SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(name||addr),id;",
|
||||
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(())
|
||||
}
|
||||
).await?;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
"SELECT id FROM contacts WHERE addr!=? AND id>? AND origin>=? AND blocked=0 ORDER BY LOWER(name || addr), id;"
|
||||
).bind(self_addr).bind(DC_CONTACT_ID_LAST_SPECIAL as i32).bind(0x100).fetch(&pool);
|
||||
|
||||
while let Some(id) = rows.next().await {
|
||||
let (id,): (i32,) = id?;
|
||||
ret.push(id as u32);
|
||||
}
|
||||
}
|
||||
|
||||
if flag_add_self && add_self {
|
||||
@@ -641,32 +653,30 @@ impl Contact {
|
||||
}
|
||||
|
||||
pub async fn get_blocked_cnt(context: &Context) -> usize {
|
||||
context
|
||||
let v: i32 = context
|
||||
.sql
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default() as usize
|
||||
.unwrap_or_default();
|
||||
v as usize
|
||||
}
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Vec<u32> {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_values(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
|row| row.get::<_, u32>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|id: i32| id as u32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a textual summary of the encryption state for the contact.
|
||||
@@ -681,9 +691,10 @@ impl Contact {
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
|
||||
if peerstate.is_some()
|
||||
if peerstate.is_ok()
|
||||
&& peerstate
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
|
||||
.is_some()
|
||||
{
|
||||
@@ -752,10 +763,9 @@ impl Contact {
|
||||
|
||||
let count_contacts: i32 = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsx![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -763,10 +773,9 @@ impl Contact {
|
||||
let count_msgs: i32 = if count_contacts > 0 {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||
paramsv![contact_id as i32, contact_id as i32],
|
||||
paramsx![contact_id as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -779,7 +788,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsx![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -817,7 +826,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id as i32],
|
||||
paramsx![self.param.to_string(), self.id as i32],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -925,7 +934,7 @@ impl Contact {
|
||||
pub async fn is_verified_ex(
|
||||
&self,
|
||||
context: &Context,
|
||||
peerstate: Option<&Peerstate<'_>>,
|
||||
peerstate: Option<&Peerstate>,
|
||||
) -> VerifiedStatus {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
@@ -940,7 +949,7 @@ impl Contact {
|
||||
}
|
||||
|
||||
let peerstate = Peerstate::from_addr(context, &self.addr).await;
|
||||
if let Some(ps) = peerstate {
|
||||
if let Ok(ps) = peerstate {
|
||||
if ps.verified_key.is_some() {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
}
|
||||
@@ -975,15 +984,15 @@ impl Contact {
|
||||
return 0;
|
||||
}
|
||||
|
||||
context
|
||||
let v: i32 = context
|
||||
.sql
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default() as usize
|
||||
.unwrap_or_default();
|
||||
v as usize
|
||||
}
|
||||
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
||||
@@ -995,7 +1004,7 @@ impl Contact {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsx![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1006,7 +1015,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
paramsx![origin, contact_id as i32, origin],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1068,7 +1077,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
paramsx![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1078,56 +1087,42 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context.sql.execute(
|
||||
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
paramsv![new_blocking, 100, contact_id as i32],
|
||||
).await.is_ok() {
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
}
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET blocked=?
|
||||
WHERE type=? AND id
|
||||
IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);
|
||||
"#,
|
||||
paramsx![new_blocking, 100i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set profile image for a contact.
|
||||
///
|
||||
/// The given profile image is expected to be already in the blob directory
|
||||
/// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
///
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
|
||||
/// this typically happens if we see message with our own profile image, sent from another device.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
// the given profile image is expected to be already in the blob directory
|
||||
// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let changed = match profile_image {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar.");
|
||||
}
|
||||
} else {
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
true
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context.set_config(Config::Selfavatar, None).await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar deletion.");
|
||||
}
|
||||
} else {
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
}
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
177
src/context.rs
177
src/context.rs
@@ -6,7 +6,6 @@ use std::ops::Deref;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||
use async_std::task;
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::config::Config;
|
||||
@@ -15,10 +14,12 @@ use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::events::{Event, EventEmitter, Events};
|
||||
use crate::job::{self, Action};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
@@ -51,13 +52,10 @@ pub struct InnerContext {
|
||||
pub(crate) generating_key_mutex: Mutex<()>,
|
||||
/// Mutex to enforce only a single running oauth2 is running.
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
|
||||
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
|
||||
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
|
||||
|
||||
creation_time: SystemTime,
|
||||
}
|
||||
@@ -78,7 +76,11 @@ 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());
|
||||
|
||||
let version =
|
||||
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
|
||||
res.insert("sqlite_version", version);
|
||||
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
res.insert("level", "awesome".into());
|
||||
res
|
||||
@@ -87,8 +89,6 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -120,21 +120,17 @@ impl Context {
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events: Events::default(),
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
ephemeral_task: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
ensure!(
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
|
||||
"Failed opening sqlite database"
|
||||
);
|
||||
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
@@ -267,27 +263,29 @@ impl Context {
|
||||
let is_configured = self.get_config_int(Config::Configured).await;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.get_raw_config_int("dbversion")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
|
||||
.query_value("PRAGMA journal_mode;", paramsx![])
|
||||
.await
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.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 prv_key_cnt: Option<isize> = self
|
||||
let prv_key_cnt: Option<i32> = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await;
|
||||
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let pub_key_cnt: Option<isize> = self
|
||||
let pub_key_cnt: Option<i32> = self
|
||||
.sql
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await;
|
||||
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
|
||||
.await
|
||||
.ok();
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
@@ -299,7 +297,7 @@ impl Context {
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int(self, "folders_configured")
|
||||
.get_raw_config_int("folders_configured")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -360,30 +358,22 @@ impl Context {
|
||||
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
|
||||
let show_deaddrop: i32 = 0;
|
||||
self.sql
|
||||
.query_map(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.state=?",
|
||||
" AND m.hidden=0",
|
||||
" AND m.chat_id>?",
|
||||
" AND ct.blocked=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for row in rows {
|
||||
ret.push(row?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
.query_values(
|
||||
r#"
|
||||
SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.state=?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>?
|
||||
AND ct.blocked=0
|
||||
AND (c.blocked=0 OR c.blocked=?)
|
||||
ORDER BY m.timestamp DESC,m.id DESC;
|
||||
"#,
|
||||
paramsx![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -399,47 +389,36 @@ impl Context {
|
||||
let strLikeBeg = format!("{}%", real_query);
|
||||
|
||||
let query = if !chat_id.is_unset() {
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" WHERE m.chat_id=?",
|
||||
" AND m.hidden=0",
|
||||
" AND ct.blocked=0",
|
||||
" AND (txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp,m.id;"
|
||||
)
|
||||
r#"
|
||||
SELECT m.id
|
||||
FROM msgs
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND ct.blocked=0
|
||||
AND (txt LIKE ? OR ct.name LIKE ?)
|
||||
ORDER BY m.timestamp,m.id;
|
||||
"#
|
||||
} else {
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.chat_id>9",
|
||||
" AND m.hidden=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" AND ct.blocked=0",
|
||||
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
)
|
||||
r#"
|
||||
SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.chat_id>9
|
||||
AND m.hidden=0
|
||||
AND (c.blocked=0 OR c.blocked=?)
|
||||
AND ct.blocked=0
|
||||
AND (m.txt LIKE ? OR ct.name LIKE ?)
|
||||
ORDER BY m.timestamp DESC,m.id DESC;
|
||||
"#
|
||||
};
|
||||
|
||||
self.sql
|
||||
.query_map(
|
||||
query,
|
||||
paramsv![chat_id, strLikeInText, strLikeBeg],
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -458,6 +437,34 @@ impl Context {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
}
|
||||
|
||||
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
||||
if !self.get_config_bool(Config::MvboxMove).await {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_mvbox(folder).await {
|
||||
return;
|
||||
}
|
||||
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return;
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => {}
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => {
|
||||
job::add(
|
||||
self,
|
||||
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use async_std::prelude::*;
|
||||
use itertools::join;
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
@@ -197,14 +196,7 @@ pub async fn dc_receive_imf(
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
match contact::set_profile_image(
|
||||
&context,
|
||||
from_id,
|
||||
avatar_action,
|
||||
mime_parser.was_encrypted(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match contact::set_profile_image(&context, from_id, avatar_action).await {
|
||||
Ok(()) => {
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
}
|
||||
@@ -231,29 +223,24 @@ pub async fn dc_receive_imf(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if insert_msg_id
|
||||
.needs_move(context, server_folder.as_ref())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
} else {
|
||||
// Move message if we don't delete it immediately.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, insert_msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), insert_msg_id)
|
||||
.await;
|
||||
if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,78 +619,16 @@ async fn add_parts(
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract ephemeral timer from the message.
|
||||
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
|
||||
match value.parse::<EphemeralTimer>() {
|
||||
Ok(timer) => timer,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"can't parse ephemeral timer \"{}\": {}", value, err
|
||||
);
|
||||
EphemeralTimer::Disabled
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EphemeralTimer::Disabled
|
||||
};
|
||||
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
// Only non-hidden timers are applied now. Timers from hidden
|
||||
// messages such as read receipts can be useful to detect
|
||||
// ephemeral timer support, but timer changes without visible
|
||||
// received messages may be confusing to the user.
|
||||
if !*hidden
|
||||
&& !location_kml_is
|
||||
&& !is_mdn
|
||||
&& (*chat_id).get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
match (*chat_id)
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
);
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
// to 1 week, and then changed to 1 hour: after 1
|
||||
// hour, only the message about the change to 1
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
} else {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
*chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// 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,
|
||||
state == MessageState::InFresh,
|
||||
)
|
||||
.await;
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// unarchive chat
|
||||
@@ -727,9 +652,7 @@ async fn add_parts(
|
||||
let icnt = mime_parser.parts.len();
|
||||
|
||||
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 location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_system_message = mime_parser.is_system_message;
|
||||
let mime_headers = if save_mime_headers {
|
||||
Some(String::from_utf8_lossy(imf_raw).to_string())
|
||||
@@ -737,59 +660,43 @@ async fn add_parts(
|
||||
None
|
||||
};
|
||||
let sent_timestamp = *sent_timestamp;
|
||||
let is_hidden = *hidden;
|
||||
let chat_id = *chat_id;
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
// TODO: can this clone be avoided?
|
||||
let rfc724_mid = rfc724_mid.to_string();
|
||||
for part in &mut mime_parser.parts {
|
||||
let mut txt_raw = "".to_string();
|
||||
|
||||
let (new_parts, ids, is_hidden) = context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let mut ids = Vec::with_capacity(parts.len());
|
||||
let mut is_hidden = is_hidden;
|
||||
let is_location_kml =
|
||||
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
|
||||
|
||||
for part in &mut parts {
|
||||
let mut txt_raw = "".to_string();
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \
|
||||
timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references, error, ephemeral_timer, ephemeral_timestamp) \
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?, ?,?,?);",
|
||||
)?;
|
||||
if is_mdn || is_location_kml {
|
||||
*hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
}
|
||||
}
|
||||
|
||||
let is_location_kml = location_kml_is
|
||||
&& icnt == 1
|
||||
&& (part.msg == "-location-" || part.msg.is_empty());
|
||||
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);
|
||||
}
|
||||
|
||||
if is_mdn || is_location_kml {
|
||||
is_hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
stmt.execute(paramsv![
|
||||
rfc724_mid,
|
||||
server_folder,
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
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, txt_raw, param,
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
|
||||
"#,
|
||||
paramsx![
|
||||
rfc724_mid.to_owned(),
|
||||
server_folder.as_ref().to_owned(),
|
||||
server_uid as i32,
|
||||
chat_id,
|
||||
from_id as i32,
|
||||
@@ -800,40 +707,30 @@ async fn add_parts(
|
||||
part.typ,
|
||||
state,
|
||||
is_dc_message,
|
||||
part.msg,
|
||||
part.msg.clone(),
|
||||
// txt_raw might contain invalid utf8
|
||||
txt_raw,
|
||||
part.param.to_string(),
|
||||
part.bytes as isize,
|
||||
is_hidden,
|
||||
mime_headers,
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
part.error,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
])?;
|
||||
part.bytes as i64,
|
||||
*hidden,
|
||||
mime_headers.clone(),
|
||||
mime_in_reply_to.clone(),
|
||||
mime_references.clone(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(stmt);
|
||||
ids.push(MsgId::new(crate::sql::get_rowid(
|
||||
&mut conn,
|
||||
"msgs",
|
||||
"rfc724_mid",
|
||||
&rfc724_mid,
|
||||
)?));
|
||||
}
|
||||
Ok((parts, ids, is_hidden))
|
||||
})
|
||||
.await?;
|
||||
let msg_id = MsgId::new(
|
||||
context
|
||||
.sql
|
||||
.get_rowid("msgs", "rfc724_mid", &rfc724_mid)
|
||||
.await?,
|
||||
);
|
||||
|
||||
if let Some(id) = ids.iter().last() {
|
||||
*insert_msg_id = *id;
|
||||
*insert_msg_id = msg_id;
|
||||
created_db_entries.push((chat_id, msg_id));
|
||||
}
|
||||
|
||||
*hidden = is_hidden;
|
||||
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
|
||||
mime_parser.parts = new_parts;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
|
||||
@@ -951,18 +848,17 @@ async fn calc_sort_timestamp(
|
||||
// get newest non fresh message for this chat
|
||||
// update sort_timestamp if less than that
|
||||
if is_fresh_msg {
|
||||
let last_msg_time: Option<i64> = context
|
||||
let last_msg_time: Result<i32, _> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
|
||||
paramsv![chat_id, MessageState::InFresh],
|
||||
paramsx![chat_id, MessageState::InFresh],
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(last_msg_time) = last_msg_time {
|
||||
if last_msg_time > sort_timestamp {
|
||||
sort_timestamp = last_msg_time;
|
||||
if let Ok(last_msg_time) = last_msg_time {
|
||||
if last_msg_time as i64 > sort_timestamp {
|
||||
sort_timestamp = last_msg_time as i64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1239,7 +1135,7 @@ async fn create_or_lookup_group(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![grpname.to_string(), chat_id],
|
||||
paramsx![grpname, chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1275,7 +1171,7 @@ async fn create_or_lookup_group(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![chat_id],
|
||||
paramsx![chat_id],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@@ -1357,10 +1253,10 @@ async fn create_or_lookup_adhoc_group(
|
||||
let chat_ids = search_chat_ids_by_contact_ids(context, &member_ids).await?;
|
||||
if !chat_ids.is_empty() {
|
||||
let chat_ids_str = join(chat_ids.iter().map(|x| x.to_string()), ",");
|
||||
let res = context
|
||||
let res: Result<(ChatId, Option<Blocked>), _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT c.id,
|
||||
c.blocked
|
||||
FROM chats c
|
||||
@@ -1372,19 +1268,13 @@ async fn create_or_lookup_adhoc_group(
|
||||
LIMIT 1;",
|
||||
chat_ids_str
|
||||
),
|
||||
paramsv![],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, ChatId>(0)?,
|
||||
row.get::<_, Option<Blocked>>(1)?.unwrap_or_default(),
|
||||
))
|
||||
},
|
||||
paramsx![],
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok((id, id_blocked)) = res {
|
||||
/* success, chat found */
|
||||
return Ok((id, id_blocked));
|
||||
return Ok((id, id_blocked.unwrap_or_default()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,7 +1306,7 @@ async fn create_or_lookup_adhoc_group(
|
||||
|
||||
// create a new ad-hoc group
|
||||
// - there is no need to check if this group exists; otherwise we would have caught it above
|
||||
let grpid = create_adhoc_grp_id(context, &member_ids).await;
|
||||
let grpid = create_adhoc_grp_id(context, &member_ids).await?;
|
||||
if grpid.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1456,7 +1346,7 @@ async fn create_group_record(
|
||||
) -> ChatId {
|
||||
if context.sql.execute(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
paramsx![
|
||||
if VerifiedStatus::Unverified != create_verified {
|
||||
Chattype::VerifiedGroup
|
||||
} else {
|
||||
@@ -1465,7 +1355,7 @@ async fn create_group_record(
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
create_blocked,
|
||||
time(),
|
||||
time()
|
||||
],
|
||||
).await
|
||||
.is_err()
|
||||
@@ -1480,7 +1370,7 @@ async fn create_group_record(
|
||||
}
|
||||
let row_id = context
|
||||
.sql
|
||||
.get_rowid(context, "chats", "grpid", grpid.as_ref())
|
||||
.get_rowid("chats", "grpid", grpid.as_ref())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -1495,7 +1385,7 @@ async fn create_group_record(
|
||||
chat_id
|
||||
}
|
||||
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<String> {
|
||||
/* algorithm:
|
||||
- sort normalized, lowercased, e-mail addresses alphabetically
|
||||
- put all e-mail addresses into a single string, separate the address by a single comma
|
||||
@@ -1509,33 +1399,22 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
|
||||
.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::<std::result::Result<Vec<_>, _>>()?;
|
||||
addrs.sort();
|
||||
let mut acc = member_cs.clone();
|
||||
for addr in &addrs {
|
||||
acc += ",";
|
||||
acc += &addr.to_lowercase();
|
||||
}
|
||||
Ok(acc)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| member_cs);
|
||||
let query = format!(
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
||||
member_ids_str
|
||||
);
|
||||
let mut addrs: Vec<String> = context.sql.query_values(&query, paramsx![]).await?;
|
||||
addrs.sort();
|
||||
|
||||
hex_hash(&members)
|
||||
let mut acc = member_cs;
|
||||
for addr in &addrs {
|
||||
acc += ",";
|
||||
acc += &addr.to_lowercase();
|
||||
}
|
||||
|
||||
Ok(hex_hash(&acc))
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn hex_hash(s: impl AsRef<str>) -> String {
|
||||
let bytes = s.as_ref().as_bytes();
|
||||
let result = Sha256::digest(bytes);
|
||||
@@ -1560,8 +1439,7 @@ async fn search_chat_ids_by_contact_ids(
|
||||
if !contact_ids.is_empty() {
|
||||
contact_ids.sort();
|
||||
let contact_ids_str = join(contact_ids.iter().map(|x| x.to_string()), ",");
|
||||
context.sql.query_map(
|
||||
format!(
|
||||
let query = format!(
|
||||
"SELECT DISTINCT cc.chat_id, cc.contact_id
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN chats c ON c.id=cc.chat_id
|
||||
@@ -1570,37 +1448,37 @@ async fn search_chat_ids_by_contact_ids(
|
||||
AND cc.contact_id!=1
|
||||
ORDER BY cc.chat_id, cc.contact_id;", // 1=DC_CONTACT_ID_SELF
|
||||
contact_ids_str
|
||||
),
|
||||
paramsv![],
|
||||
|row| Ok((row.get::<_, ChatId>(0)?, row.get::<_, u32>(1)?)),
|
||||
|rows| {
|
||||
let mut last_chat_id = ChatId::new(0);
|
||||
let mut matches = 0;
|
||||
let mut mismatches = 0;
|
||||
);
|
||||
|
||||
for row in rows {
|
||||
let (chat_id, contact_id) = row?;
|
||||
if chat_id != last_chat_id {
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
last_chat_id = chat_id;
|
||||
matches = 0;
|
||||
mismatches = 0;
|
||||
}
|
||||
if contact_ids.get(matches) == Some(&contact_id) {
|
||||
matches += 1;
|
||||
} else {
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(&query).fetch(&pool);
|
||||
|
||||
let mut last_chat_id = ChatId::new(0);
|
||||
let mut matches = 0;
|
||||
let mut mismatches = 0;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id, contact_id): (ChatId, i32) = row?;
|
||||
let contact_id = contact_id as u32;
|
||||
|
||||
if chat_id != last_chat_id {
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
Ok(())
|
||||
last_chat_id = chat_id;
|
||||
matches = 0;
|
||||
mismatches = 0;
|
||||
}
|
||||
).await?;
|
||||
if matches < contact_ids.len() && contact_id == contact_ids[matches] {
|
||||
matches += 1;
|
||||
} else {
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1624,8 +1502,10 @@ async fn check_verified_properties(
|
||||
if from_id != DC_CONTACT_ID_SELF {
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await;
|
||||
|
||||
if peerstate.is_none()
|
||||
|| contact.is_verified_ex(context, peerstate.as_ref()).await
|
||||
if peerstate.is_err()
|
||||
|| contact
|
||||
.is_verified_ex(context, peerstate.as_ref().ok())
|
||||
.await
|
||||
!= VerifiedStatus::BidirectVerified
|
||||
{
|
||||
bail!(
|
||||
@@ -1634,7 +1514,7 @@ async fn check_verified_properties(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Ok(peerstate) = peerstate {
|
||||
ensure!(
|
||||
peerstate.has_verified_key(&mimeparser.signatures),
|
||||
"The message was sent with non-verified encryption."
|
||||
@@ -1651,36 +1531,30 @@ 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 query = 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::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
to_ids_str
|
||||
);
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(&query).fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (to_addr, is_verified): (String, i32) = row?;
|
||||
|
||||
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 mut is_verified = is_verified != 0;
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await;
|
||||
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossipped_addr.contains(&to_addr) {
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
if let Ok(mut peerstate) = peerstate {
|
||||
// if we're here, we know the gossip key is verified:
|
||||
// - use the gossip-key as verified-key if there is no verified-key
|
||||
// - OR if the verified-key does not match public-key or gossip-key
|
||||
@@ -1716,11 +1590,10 @@ async fn check_verified_properties(
|
||||
|
||||
fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
|
||||
let msg = better_msg.as_ref();
|
||||
if !msg.is_empty() {
|
||||
if let Some(part) = mime_parser.parts.get_mut(0) {
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
}
|
||||
if !msg.is_empty() && !mime_parser.parts.is_empty() {
|
||||
let part = &mut mime_parser.parts[0];
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1771,7 +1644,7 @@ async fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
LEFT JOIN chats c ON m.chat_id=c.id \
|
||||
WHERE m.rfc724_mid=? \
|
||||
AND m.chat_id>9 AND c.blocked=0;",
|
||||
paramsv![rfc724_mid],
|
||||
paramsx![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1817,7 +1690,7 @@ async fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
|
||||
paramsv![rfc724_mid],
|
||||
paramsx![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1879,7 +1752,7 @@ fn dc_create_incoming_rfc724_mid(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, ChatVisibility};
|
||||
use crate::chat::ChatVisibility;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::*;
|
||||
@@ -2088,14 +1961,32 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.name, "Bob");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 1);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 1);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, 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.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 2);
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, 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.ctx, GRP_MAIL, "INBOX", 3, false)
|
||||
@@ -2109,7 +2000,13 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -2133,7 +2030,13 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -2159,7 +2062,10 @@ mod tests {
|
||||
.unwrap();
|
||||
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
group_id
|
||||
@@ -2202,13 +2108,11 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await;
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2257,7 +2161,10 @@ mod tests {
|
||||
)
|
||||
.await.unwrap();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
@@ -2341,13 +2248,9 @@ mod tests {
|
||||
.get_authname(),
|
||||
"Имя, Фамилия",
|
||||
);
|
||||
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(), 1);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2609,13 +2512,12 @@ mod tests {
|
||||
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None).await;
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msg_id).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msgs.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
last_msg.text,
|
||||
|
||||
@@ -22,7 +22,6 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
|
||||
@@ -55,7 +54,6 @@ const COLORS: [u32; 16] = [
|
||||
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
|
||||
];
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
||||
let str_lower = s.as_ref().to_lowercase();
|
||||
let mut checksum = 0;
|
||||
@@ -200,7 +198,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
|
||||
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
|
||||
let hostname = from_addr
|
||||
.find('@')
|
||||
.and_then(|k| from_addr.get(k..))
|
||||
.map(|k| &from_addr[k..])
|
||||
.unwrap_or("@nohost");
|
||||
match grpid {
|
||||
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
|
||||
@@ -570,14 +568,6 @@ impl FromStr for EmailAddress {
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility to check if a in the binary represantion of listflags
|
||||
/// the bit at position bitindex is 1.
|
||||
pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
|
||||
|
||||
46
src/e2ee.rs
46
src/e2ee.rs
@@ -91,7 +91,7 @@ impl EncryptHelper {
|
||||
context: &Context,
|
||||
min_verified: PeerstateVerifiedStatus,
|
||||
mail_to_encrypt: lettre_email::PartBuilder,
|
||||
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
|
||||
peerstates: Vec<(Option<Peerstate>, &str)>,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
|
||||
@@ -115,12 +115,6 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an
|
||||
/// Autocrypt message, i.e. encrypted and signed with a valid
|
||||
/// signature.
|
||||
///
|
||||
/// Returns decrypted body and a set of valid signature fingerprints
|
||||
/// if successful.
|
||||
pub async fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
@@ -138,7 +132,7 @@ pub async fn try_decrypt(
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await;
|
||||
peerstate = Peerstate::from_addr(context, &from).await.ok();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
@@ -149,7 +143,7 @@ pub async fn try_decrypt(
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
} else if let Some(ref header) = autocryptheader {
|
||||
let p = Peerstate::from_header(context, header, message_time);
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
@@ -161,7 +155,7 @@ pub async fn try_decrypt(
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await;
|
||||
peerstate = Peerstate::from_addr(&context, &from).await.ok();
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
@@ -193,23 +187,24 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
|
||||
"Not a multipart/encrypted message: {}",
|
||||
mail.ctype.mimetype
|
||||
);
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
ensure!(
|
||||
first_part.ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
first_part.ctype,
|
||||
);
|
||||
ensure!(
|
||||
mail.subparts.len() == 2,
|
||||
"Invalid Autocrypt Level 1 Mime Parts"
|
||||
);
|
||||
|
||||
ensure!(
|
||||
second_part.ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
second_part.ctype
|
||||
);
|
||||
ensure!(
|
||||
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
mail.subparts[0].ctype,
|
||||
);
|
||||
|
||||
Ok(second_part)
|
||||
} else {
|
||||
bail!("Invalid Autocrypt Level 1 Mime Parts")
|
||||
}
|
||||
ensure!(
|
||||
mail.subparts[1].ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
mail.subparts[1].ctype
|
||||
);
|
||||
|
||||
Ok(&mail.subparts[1])
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
@@ -272,7 +267,6 @@ async fn decrypt_part(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
||||
if let Some(index) = input.iter().position(|b| *b > b' ') {
|
||||
if input.len() - index > 26 {
|
||||
|
||||
528
src/ephemeral.rs
528
src/ephemeral.rs
@@ -1,528 +0,0 @@
|
||||
//! # Ephemeral messages
|
||||
//!
|
||||
//! Ephemeral messages are messages that have an Ephemeral-Timer
|
||||
//! header attached to them, which specifies time in seconds after
|
||||
//! which the message should be deleted both from the device and from
|
||||
//! the server. The timer is started when the message is marked as
|
||||
//! seen, which usually happens when its contents is displayed on
|
||||
//! device screen.
|
||||
//!
|
||||
//! Each chat, including 1:1, group chats and "saved messages" chat,
|
||||
//! has its own ephemeral timer setting, which is applied to all
|
||||
//! messages sent to the chat. The setting is synchronized to all the
|
||||
//! devices participating in the chat by applying the timer value from
|
||||
//! all received messages, including BCC-self ones, to the chat. This
|
||||
//! way the setting is eventually synchronized among all participants.
|
||||
//!
|
||||
//! When user changes ephemeral timer setting for the chat, a system
|
||||
//! message is automatically sent to update the setting for all
|
||||
//! participants. This allows changing the setting for a chat like any
|
||||
//! group chat setting, e.g. name and avatar, without the need to
|
||||
//! write an actual message.
|
||||
//!
|
||||
//! ## Device settings
|
||||
//!
|
||||
//! In addition to per-chat ephemeral message setting, each device has
|
||||
//! two global user-configured settings that complement per-chat
|
||||
//! settings: `delete_device_after` and `delete_server_after`. These
|
||||
//! settings are not synchronized among devices and apply to all
|
||||
//! messages known to the device, including messages sent or received
|
||||
//! before configuring the setting.
|
||||
//!
|
||||
//! `delete_device_after` configures the maximum time device is
|
||||
//! storing the messages locally. `delete_server_after` configures the
|
||||
//! time after which device will delete the messages it knows about
|
||||
//! from the server.
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
//! When the message is deleted locally, its contents is removed and
|
||||
//! it is moved to the trash chat. This database entry is then used to
|
||||
//! track the Message-ID and corresponding IMAP folder and UID until
|
||||
//! the message is deleted from the server. Vice versa, when device
|
||||
//! deletes the message from the server, it removes IMAP folder and
|
||||
//! UID information, but keeps the message contents. When database
|
||||
//! entry is both moved to trash chat and does not contain UID
|
||||
//! information, it is deleted from the database, leaving no trace of
|
||||
//! the message.
|
||||
//!
|
||||
//! ## When messages are deleted
|
||||
//!
|
||||
//! Local deletion happens when the chatlist or chat is loaded. A
|
||||
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
||||
//! make UI reload displayed messages and cause actual deletion.
|
||||
//!
|
||||
//! Server deletion happens by generating IMAP deletion jobs based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
|
||||
use crate::constants::{
|
||||
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
Disabled,
|
||||
Enabled { duration: u32 },
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn to_u32(self) -> u32 {
|
||||
match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => duration,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_u32(duration: u32) -> Self {
|
||||
if duration == 0 {
|
||||
Self::Disabled
|
||||
} else {
|
||||
Self::Enabled { duration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Timer {
|
||||
fn default() -> Self {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Timer {
|
||||
fn to_string(&self) -> String {
|
||||
self.to_u32().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Timer {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(input: &str) -> Result<Timer, ParseIntError> {
|
||||
input.parse::<u32>().map(Self::from_u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for Timer {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => i64::from(*duration),
|
||||
});
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for Timer {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|value| {
|
||||
if value == 0 {
|
||||
Ok(Self::Disabled)
|
||||
} else if let Ok(duration) = u32::try_from(value) {
|
||||
Ok(Self::Enabled { duration })
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatId {
|
||||
/// Get ephemeral message timer value in seconds.
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Set ephemeral timer value without sending a message.
|
||||
///
|
||||
/// Used when a message arrives indicating that someone else has
|
||||
/// changed the timer value for a chat.
|
||||
pub(crate) async fn inner_set_ephemeral_timer(
|
||||
self,
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
) -> Result<(), Error> {
|
||||
ensure!(!self.is_special(), "Invalid chat ID");
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::ChatEphemeralTimerModified {
|
||||
chat_id: self,
|
||||
timer,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set ephemeral message timer value in seconds.
|
||||
///
|
||||
/// If timer value is 0, disable ephemeral message timer.
|
||||
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
|
||||
if timer == self.get_ephemeral_timer(context).await? {
|
||||
return Ok(());
|
||||
}
|
||||
self.inner_set_ephemeral_timer(context, timer).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
|
||||
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
|
||||
if let Err(err) = send_msg(context, self, &mut msg).await {
|
||||
error!(
|
||||
context,
|
||||
"Failed to send a message about ephemeral message timer change: {:?}", err
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
|
||||
pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
from_id: u32,
|
||||
) -> String {
|
||||
let stock_message = match timer {
|
||||
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
60 => StockMessage::MsgEphemeralTimerMinute,
|
||||
3600 => StockMessage::MsgEphemeralTimerHour,
|
||||
86400 => StockMessage::MsgEphemeralTimerDay,
|
||||
604_800 => StockMessage::MsgEphemeralTimerWeek,
|
||||
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
|
||||
_ => StockMessage::MsgEphemeralTimerEnabled,
|
||||
},
|
||||
};
|
||||
|
||||
context
|
||||
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
|
||||
.await
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
Some(duration) => Timer::Enabled { 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<()> {
|
||||
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 = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||
)
|
||||
.await?;
|
||||
schedule_ephemeral_task(context).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Returns true if any message is deleted, so caller can emit
|
||||
/// MsgsChanged event. If nothing has been deleted, returns
|
||||
/// false. This function does not emit the MsgsChanged event itself,
|
||||
/// because it is also called when chatlist is reloaded, and emitting
|
||||
/// MsgsChanged there will cause infinite reload loop.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
let mut updated = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE \
|
||||
ephemeral_timestamp != 0 \
|
||||
AND ephemeral_timestamp < ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
|
||||
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()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
}
|
||||
|
||||
schedule_ephemeral_task(context).await;
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Schedule a task to emit MsgsChanged event when the next local
|
||||
/// deletion happens. Existing task is cancelled to make sure at most
|
||||
/// one such task is scheduled at a time.
|
||||
///
|
||||
/// UI is expected to reload the chatlist or the chat in response to
|
||||
/// MsgsChanged event, this will trigger actual deletion.
|
||||
///
|
||||
/// This takes into account only per-chat timeouts, because global device
|
||||
/// timeouts are at least one hour long and deletion is triggered often enough
|
||||
/// by user actions.
|
||||
pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let ephemeral_timestamp: Option<i64> = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timestamp \
|
||||
FROM msgs \
|
||||
WHERE ephemeral_timestamp != 0 \
|
||||
AND chat_id != ? \
|
||||
ORDER BY ephemeral_timestamp ASC \
|
||||
LIMIT 1",
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
|
||||
return;
|
||||
}
|
||||
Ok(ephemeral_timestamp) => ephemeral_timestamp,
|
||||
};
|
||||
|
||||
// Cancel existing task, if any
|
||||
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
|
||||
ephemeral_task.cancel().await;
|
||||
}
|
||||
|
||||
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
|
||||
let now = SystemTime::now();
|
||||
let until = UNIX_EPOCH
|
||||
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
|
||||
+ Duration::from_secs(1);
|
||||
|
||||
if let Ok(duration) = until.duration_since(now) {
|
||||
// Schedule a task, ephemeral_timestamp is in the future
|
||||
let context1 = context.clone();
|
||||
let ephemeral_task = task::spawn(async move {
|
||||
async_std::task::sleep(duration).await;
|
||||
emit_event!(
|
||||
context1,
|
||||
Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
});
|
||||
*context.ephemeral_task.write().await = Some(ephemeral_task);
|
||||
} else {
|
||||
// Emit event immediately
|
||||
emit_event!(
|
||||
context,
|
||||
Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns ID of any expired message that should be deleted from the server.
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
let now = time();
|
||||
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"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),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
/// yet.
|
||||
///
|
||||
/// It is possible that timers are not started due to a missing or
|
||||
/// failed `MsgId.start_ephemeral_timer()` call, either in the current
|
||||
/// or previous version of Delta Chat.
|
||||
///
|
||||
/// This function is supposed to be called in the background,
|
||||
/// e.g. from housekeeping task.
|
||||
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"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
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
|
||||
"Message deletion timer is disabled by me."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
|
||||
"Message deletion timer is disabled."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
|
||||
"Message deletion timer is set to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
|
||||
"Message deletion timer is set to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 4 weeks."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use async_std::sync::{channel, Receiver, Sender, TrySendError};
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -190,19 +189,9 @@ pub enum Event {
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
/// and dc_remove_contact_from_chat().
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[strum(props(id = "2020"))]
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[strum(props(id = "2021"))]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: ChatId,
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
|
||||
@@ -42,7 +42,6 @@ pub enum HeaderDef {
|
||||
SecureJoinFingerprint,
|
||||
SecureJoinInvitenumber,
|
||||
SecureJoinAuth,
|
||||
EphemeralTimer,
|
||||
_TestHeader,
|
||||
}
|
||||
|
||||
|
||||
225
src/imap/idle.rs
225
src/imap/idle.rs
@@ -1,7 +1,6 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::types::UnsolicitedResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -14,19 +13,19 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
|
||||
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP IDLE protocol timed out: {0}")]
|
||||
#[error("IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[error("IMAP select folder error: {0}")]
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[error("Setup handle error: {0}")]
|
||||
#[error("Setup handle error")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
}
|
||||
|
||||
@@ -49,27 +48,11 @@ impl Imap {
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
let session = self.session.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if let Some(session) = self.session.take() {
|
||||
// if we have unsolicited responses we directly return
|
||||
let mut unsolicited_exists = false;
|
||||
while let Ok(response) = session.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
UnsolicitedResponse::Exists(_) => {
|
||||
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
|
||||
unsolicited_exists = true;
|
||||
}
|
||||
_ => info!(context, "ignoring unsolicited response {:?}", response),
|
||||
}
|
||||
}
|
||||
|
||||
if unsolicited_exists {
|
||||
self.session = Some(session);
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
if let Some(session) = session {
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
@@ -82,43 +65,68 @@ impl Imap {
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let probe_network = self.idle_interrupt.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
drop(idle_wait);
|
||||
drop(interrupt);
|
||||
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
});
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
|
||||
self.idle_interrupt.recv().map(|probe_network| {
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
}),
|
||||
);
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "Idle has NewData {:?}", x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info = i;
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info = i;
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session = handle
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res = handle
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.await
|
||||
.map_err(Error::IdleTimeout)??;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
.map_err(|err| {
|
||||
self.trigger_reconnect();
|
||||
Error::IdleTimeout(err)
|
||||
})?;
|
||||
|
||||
match res {
|
||||
Ok(session) => {
|
||||
self.session = Some(Session { inner: session });
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
@@ -140,66 +148,73 @@ impl Imap {
|
||||
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||
}
|
||||
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut info: InterruptInfo = Default::default();
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
let info = loop {
|
||||
use futures::future::FutureExt;
|
||||
match interval
|
||||
.next()
|
||||
.map(|_| Event::Tick)
|
||||
.race(
|
||||
self.idle_interrupt
|
||||
.recv()
|
||||
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Event::Tick => {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
info =
|
||||
loop {
|
||||
use futures::future::FutureExt;
|
||||
match interval
|
||||
.next()
|
||||
.map(|_| Event::Tick)
|
||||
.race(self.idle_interrupt.recv().map(|probe_network| {
|
||||
Event::Interrupt(probe_network.unwrap_or_default())
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Event::Tick => {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
|
||||
114
src/imap/mod.rs
114
src/imap/mod.rs
@@ -3,6 +3,8 @@
|
||||
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
|
||||
//! to implement connect, fetch, delete functionality with standard IMAP servers.
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use async_imap::{
|
||||
@@ -23,12 +25,12 @@ use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{self, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid, MessageState};
|
||||
use crate::message::{self, update_server_uid};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
|
||||
use crate::{scheduler::InterruptInfo, stock::StockMessage};
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
@@ -36,7 +38,6 @@ pub mod select_folder;
|
||||
mod session;
|
||||
|
||||
use client::Client;
|
||||
use message::Message;
|
||||
use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -116,8 +117,8 @@ pub struct Imap {
|
||||
session: Option<Session>,
|
||||
connected: bool,
|
||||
interrupt: Option<stop_token::StopSource>,
|
||||
skip_next_idle_wait: bool,
|
||||
should_reconnect: bool,
|
||||
login_failed_once: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -190,8 +191,8 @@ impl Imap {
|
||||
session: Default::default(),
|
||||
connected: Default::default(),
|
||||
interrupt: Default::default(),
|
||||
skip_next_idle_wait: Default::default(),
|
||||
should_reconnect: Default::default(),
|
||||
login_failed_once: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,40 +297,18 @@ impl Imap {
|
||||
// needs to be set here to ensure it is set on reconnects.
|
||||
self.connected = true;
|
||||
self.session = Some(session);
|
||||
self.login_failed_once = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.imap_user.to_owned();
|
||||
let message = context
|
||||
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
|
||||
.await;
|
||||
|
||||
warn!(context, "{} ({})", message, err);
|
||||
emit_event!(context, Event::ErrorNetwork(message.clone()));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
}
|
||||
@@ -494,7 +473,7 @@ impl Imap {
|
||||
folder: S,
|
||||
) -> (u32, u32) {
|
||||
let key = format!("imap.mailbox.{}", folder.as_ref());
|
||||
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
(
|
||||
@@ -804,6 +783,7 @@ impl Imap {
|
||||
let mut last_uid = None;
|
||||
let mut count = 0;
|
||||
|
||||
let mut tasks = Vec::with_capacity(server_uids.len());
|
||||
while let Some(Ok(msg)) = msgs.next().await {
|
||||
let server_uid = msg.uid.unwrap_or_default();
|
||||
|
||||
@@ -823,17 +803,31 @@ impl Imap {
|
||||
let context = context.clone();
|
||||
let folder = folder.clone();
|
||||
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
let task = async_std::task::spawn(async move {
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => last_uid = Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
for task in futures::future::join_all(tasks).await {
|
||||
match task {
|
||||
Some(uid) => {
|
||||
last_uid = Some(uid);
|
||||
}
|
||||
None => {
|
||||
read_errors += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if count != server_uids.len() {
|
||||
@@ -1135,10 +1129,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(());
|
||||
}
|
||||
@@ -1304,7 +1295,7 @@ impl Imap {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
|
||||
paramsv![folder],
|
||||
paramsx![folder],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1366,26 +1357,14 @@ async fn precheck_imf(
|
||||
let delete_server_after = context.get_config_delete_server_after().await;
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
if msg_id
|
||||
.needs_move(context, server_folder)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If the bcc-self message is not moved, directly
|
||||
// add MarkSeen job, otherwise MarkSeen job is
|
||||
// added after the Move Job completed.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), msg_id)
|
||||
.await;
|
||||
} else {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(
|
||||
@@ -1420,13 +1399,6 @@ async fn precheck_imf(
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
|
||||
if let Ok(MessageState::InSeen) = msg_id.get_state(context).await {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
|
||||
.await;
|
||||
|
||||
@@ -51,14 +51,6 @@ impl Imap {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Issues a CLOSE command if selected folder needs expunge.
|
||||
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
|
||||
if self.config.selected_folder_needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||
@@ -84,7 +76,10 @@ impl Imap {
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
self.maybe_close_folder(context).await?;
|
||||
let needs_expunge = { self.config.selected_folder_needs_expunge };
|
||||
if needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
|
||||
227
src/imex.rs
227
src/imex.rs
@@ -96,21 +96,21 @@ pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let sql = Sql::new();
|
||||
if sql.open(context, &path, true).await {
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int(context, "backup_time")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_time = curr_backup_time;
|
||||
}
|
||||
info!(context, "backup_time of {} is {}", name, curr_backup_time);
|
||||
sql.close().await;
|
||||
sql.open(context, &path, true).await?;
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int("backup_time")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_time = curr_backup_time;
|
||||
}
|
||||
info!(context, "backup_time of {} is {}", name, curr_backup_time);
|
||||
sql.close().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match newest_backup_path {
|
||||
Some(path) => Ok(path.to_string_lossy().into_owned()),
|
||||
None => bail!("no backup found in {}", dir_name.display()),
|
||||
@@ -178,11 +178,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
///
|
||||
/// The `passphrase` must be at least 2 characters long.
|
||||
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
|
||||
passphrase_begin
|
||||
} else {
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
ensure!(
|
||||
passphrase.len() >= 2,
|
||||
"Passphrase must be at least 2 chars long."
|
||||
);
|
||||
let private_key = SignedSecretKey::load_self(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
||||
false => None,
|
||||
@@ -197,7 +196,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
"Passphrase-Format: numeric9x4\r\n",
|
||||
"Passphrase-Begin: {}"
|
||||
),
|
||||
passphrase_begin
|
||||
&passphrase[..2]
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
@@ -246,7 +245,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(
|
||||
@@ -397,6 +396,7 @@ async fn imex_inner(
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "IMEX FAILED: {}", err);
|
||||
context.emit_event(Event::ImexProgress(0));
|
||||
bail!("IMEX FAILED to complete: {}", err);
|
||||
}
|
||||
@@ -429,57 +429,35 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
);
|
||||
/* error already logged */
|
||||
/* re-open copied database file */
|
||||
ensure!(
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await,
|
||||
"could not re-open db"
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await?;
|
||||
|
||||
delete_and_reset_all_device_msgs(&context).await?;
|
||||
|
||||
let total_files_cnt = context
|
||||
let total_files_cnt: i32 = context
|
||||
.sql
|
||||
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||
.query_value("SELECT COUNT(*) FROM backup_blobs;", paramsx![])
|
||||
.await
|
||||
.unwrap_or_default() as usize;
|
||||
.unwrap_or_default();
|
||||
let total_files_cnt = total_files_cnt as usize;
|
||||
info!(
|
||||
context,
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
);
|
||||
|
||||
// Load IDs only for now, without the file contents, to avoid
|
||||
// consuming too much memory.
|
||||
let file_ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM backup_blobs ORDER BY id",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.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
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
paramsv![file_id],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.await?;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut files = sqlx::query_as("SELECT file_name, file_content FROM backup_blobs ORDER BY id;")
|
||||
.fetch(&pool);
|
||||
|
||||
let mut processed_files_cnt = 0;
|
||||
while let Some(files_result) = files.next().await {
|
||||
let (file_name, file_blob): (String, Vec<u8>) = files_result?;
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
|
||||
if permille < 10 {
|
||||
permille = 10
|
||||
@@ -494,19 +472,22 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
|
||||
processed_files_cnt += 1;
|
||||
}
|
||||
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
}
|
||||
ensure!(
|
||||
processed_files_cnt == total_files_cnt,
|
||||
"received stop signal"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsx![])
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsx![]).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -522,9 +503,9 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
|
||||
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
|
||||
|
||||
sql::housekeeping(context).await;
|
||||
sql::housekeeping(context).await?;
|
||||
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
context.sql.execute("VACUUM;", paramsx![]).await.ok();
|
||||
|
||||
// we close the database during the copy of the dbfile
|
||||
context.sql.close().await;
|
||||
@@ -538,7 +519,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
if !copied {
|
||||
bail!(
|
||||
@@ -548,17 +529,9 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
);
|
||||
}
|
||||
let dest_sql = Sql::new();
|
||||
ensure!(
|
||||
dest_sql.open(context, &dest_path_filename, false).await,
|
||||
"could not open exported database {}",
|
||||
dest_path_string
|
||||
);
|
||||
dest_sql.open(context, &dest_path_filename, false).await?;
|
||||
|
||||
let res = match add_files_to_export(context, &dest_sql).await {
|
||||
Err(err) => {
|
||||
dc_delete_file(context, &dest_path_filename).await;
|
||||
error!(context, "backup failed: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
Ok(()) => {
|
||||
dest_sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
@@ -566,6 +539,11 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
context.emit_event(Event::ImexFileWritten(dest_path_filename));
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
dc_delete_file(context, &dest_path_filename).await;
|
||||
error!(context, "backup failed: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
};
|
||||
dest_sql.close().await;
|
||||
|
||||
@@ -578,7 +556,7 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
if !sql.table_exists("backup_blobs").await? {
|
||||
sql.execute(
|
||||
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
|
||||
paramsv![],
|
||||
paramsx![],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -590,41 +568,38 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
|
||||
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
|
||||
|
||||
sql.with_conn_async(|conn| async move {
|
||||
// scan directory, pass 2: copy files
|
||||
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||
// scan directory, pass 2: copy files
|
||||
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
|
||||
|
||||
let mut processed_files_cnt = 0;
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let entry = entry?;
|
||||
if context.shall_stop_ongoing().await {
|
||||
return Ok(());
|
||||
}
|
||||
processed_files_cnt += 1;
|
||||
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
let mut processed_files_cnt = 0;
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let entry = entry?;
|
||||
if context.shall_stop_ongoing().await {
|
||||
return Ok(());
|
||||
}
|
||||
processed_files_cnt += 1;
|
||||
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
continue;
|
||||
}
|
||||
info!(context, "EXPORT: copying filename={}", name);
|
||||
let curr_path_filename = context.get_blobdir().join(entry.file_name());
|
||||
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
|
||||
if buf.is_empty() {
|
||||
continue;
|
||||
}
|
||||
info!(context, "EXPORT: copying filename={}", name);
|
||||
let curr_path_filename = context.get_blobdir().join(entry.file_name());
|
||||
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
|
||||
if buf.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// bail out if we can't insert
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|
||||
)?;
|
||||
stmt.execute(paramsv![name, buf])?;
|
||||
}
|
||||
// bail out if we can't insert
|
||||
sql.execute(
|
||||
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|
||||
paramsx![name.as_ref(), buf],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -687,30 +662,19 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let mut export_errors = 0;
|
||||
|
||||
let keys = context
|
||||
.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<u8> = row.get(1)?;
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||
let is_default: i32 = row.get(3)?;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
|
||||
Ok((id, public_key, private_key, is_default))
|
||||
},
|
||||
|keys| {
|
||||
keys.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let mut keys = sqlx::query_as("SELECT id, public_key, private_key, is_default FROM keypairs;")
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(keys_result) = keys.next().await {
|
||||
let (id, public_key_blob, private_key_blob, is_default): (i64, Vec<u8>, Vec<u8>, i32) =
|
||||
keys_result?;
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||
|
||||
for (id, public_key, private_key, is_default) in keys {
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
|
||||
if let Ok(key) = public_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key)
|
||||
.await
|
||||
@@ -734,6 +698,7 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
}
|
||||
|
||||
ensure!(export_errors == 0, "errors while exporting keys");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
218
src/job.rs
218
src/job.rs
@@ -6,13 +6,13 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use async_smtp::smtp::response::Category;
|
||||
use async_smtp::smtp::response::Code;
|
||||
use async_smtp::smtp::response::Detail;
|
||||
use async_std::prelude::*;
|
||||
use deltachat_derive::*;
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
@@ -21,7 +21,6 @@ use crate::constants::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
@@ -38,7 +37,7 @@ use crate::{scheduler::InterruptInfo, sql};
|
||||
const JOB_RETRIES: u32 = 17;
|
||||
|
||||
/// Thread IDs
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[repr(i32)]
|
||||
pub(crate) enum Thread {
|
||||
Unknown = 0,
|
||||
@@ -76,17 +75,7 @@ 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,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Action {
|
||||
@@ -156,6 +145,32 @@ impl fmt::Display for Job {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Job {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
let foreign_id: i32 = row.try_get("foreign_id")?;
|
||||
if foreign_id < 0 {
|
||||
return Err(sqlx::Error::Decode(
|
||||
anyhow::anyhow!("invalid foreign_id").into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Job {
|
||||
job_id: row.try_get::<i32, _>("id")? as u32,
|
||||
action: row.try_get("action")?,
|
||||
foreign_id: foreign_id as u32,
|
||||
desired_timestamp: row.try_get_unchecked("desired_timestamp")?,
|
||||
added_timestamp: row.try_get_unchecked("added_timestamp")?,
|
||||
tries: row.try_get::<i32, _>("tries")? as u32,
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
pending_error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
pub fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
|
||||
let timestamp = time();
|
||||
@@ -181,7 +196,7 @@ impl Job {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsx![self.job_id as i32])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -201,22 +216,22 @@ impl Job {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
paramsv![
|
||||
paramsx![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
self.param.to_string(),
|
||||
self.job_id as i32,
|
||||
self.job_id as i32
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
paramsx![
|
||||
self.added_timestamp,
|
||||
thread,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.foreign_id as i32,
|
||||
self.param.to_string(),
|
||||
self.desired_timestamp
|
||||
]
|
||||
@@ -395,40 +410,32 @@ impl Job {
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
) -> sql::Result<(Vec<u32>, Vec<String>)> {
|
||||
// Extract message IDs from job parameters
|
||||
let res: Vec<(u32, MsgId)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
|
||||
paramsv![contact_id, self.job_id],
|
||||
|row| {
|
||||
let job_id: u32 = row.get(0)?;
|
||||
let params_str: String = row.get(1)?;
|
||||
let params: Params = params_str.parse().unwrap_or_default();
|
||||
Ok((job_id, params))
|
||||
},
|
||||
|jobs| {
|
||||
let res = jobs
|
||||
.filter_map(|row| {
|
||||
let (job_id, params) = row.ok()?;
|
||||
let msg_id = params.get_msg_id()?;
|
||||
Some((job_id, msg_id))
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Load corresponding RFC724 message IDs
|
||||
let mut job_ids = Vec::new();
|
||||
let mut rfc724_mids = Vec::new();
|
||||
for (job_id, msg_id) in res {
|
||||
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
|
||||
job_ids.push(job_id);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
|
||||
let mut rows = sqlx::query_as("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
|
||||
.bind(contact_id as i64)
|
||||
.bind(self.job_id as i64)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (job_id, params): (i64, String) = row?;
|
||||
let params: Params = params.parse().unwrap_or_default();
|
||||
let msg_id = params.get_msg_id().unwrap_or_default();
|
||||
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(Message { rfc724_mid, .. }) => {
|
||||
job_ids.push(job_id as u32);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load mdn job message: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((job_ids, rfc724_mids))
|
||||
}
|
||||
|
||||
@@ -639,21 +646,7 @@ impl Job {
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
match result {
|
||||
match imap.set_seen(context, folder, msg.server_uid).await {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
@@ -679,21 +672,27 @@ impl Job {
|
||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsx![action])
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
||||
use sqlx::Arguments;
|
||||
|
||||
let mut args = sqlx::sqlite::SqliteArguments::default();
|
||||
for job_id in job_ids {
|
||||
args.add(*job_id as i32);
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
),
|
||||
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -702,7 +701,7 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
||||
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
|
||||
.exists("SELECT id FROM jobs WHERE action=?;", paramsx![action])
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -711,11 +710,7 @@ async fn set_delivered(context: &Context, msg_id: MsgId) {
|
||||
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT chat_id FROM msgs WHERE id=?",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.query_value("SELECT chat_id FROM msgs WHERE id=?", paramsx![])
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
context.emit_event(Event::MsgDelivered { chat_id, msg_id });
|
||||
@@ -843,6 +838,25 @@ pub(crate) enum Connection<'a> {
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await {
|
||||
let threshold_timestamp = time() - delete_server_after;
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_value_optional(
|
||||
r#"
|
||||
SELECT id FROM msgs
|
||||
WHERE timestamp < ? AND server_uid != 0
|
||||
"#,
|
||||
paramsx![threshold_timestamp],
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||
Some(Job::new(
|
||||
@@ -971,12 +985,17 @@ async fn perform_job_action(
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context).await;
|
||||
if let Err(err) = sql::housekeeping(context).await {
|
||||
error!(context, "housekeeping failed: {}", err);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
};
|
||||
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
info!(
|
||||
context,
|
||||
"Inbox finished immediate try {} of job {}", tries, job
|
||||
);
|
||||
|
||||
try_res
|
||||
}
|
||||
@@ -1060,9 +1079,8 @@ pub(crate) async fn load_next(
|
||||
info!(context, "loading job for {}-thread", thread);
|
||||
|
||||
let query;
|
||||
let params;
|
||||
let params: Box<dyn Fn() -> sqlx::sqlite::SqliteArguments<'static> + 'static + Send>;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
@@ -1073,8 +1091,7 @@ WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
params = Box::new(move || paramsx![thread_i, msg_id]);
|
||||
} else if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
@@ -1085,7 +1102,7 @@ WHERE thread=? AND desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i, t];
|
||||
params = Box::new(move || paramsx![thread_i, t]);
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
@@ -1097,27 +1114,12 @@ WHERE thread=? AND tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i];
|
||||
params = Box::new(move || paramsx![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;
|
||||
let job: Option<Job> = loop {
|
||||
let p = params();
|
||||
let job_res = context.sql.query_row_optional(query, p).await;
|
||||
|
||||
match job_res {
|
||||
Ok(job) => break job,
|
||||
@@ -1126,15 +1128,13 @@ LIMIT 1;
|
||||
info!(context, "cleaning up job, because of {}", err);
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
match context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.await
|
||||
{
|
||||
let p = params();
|
||||
let id: Result<i32, _> = context.sql.query_value(query, p).await;
|
||||
match id {
|
||||
Ok(id) => {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsx![id])
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -1184,7 +1184,7 @@ mod tests {
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
paramsx![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
Action::MoveMsg,
|
||||
|
||||
57
src/key.rs
57
src/key.rs
@@ -116,22 +116,21 @@ impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
let res: std::result::Result<Vec<u8>, _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_value(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
match res {
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
@@ -161,22 +160,21 @@ impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
match context
|
||||
let res: std::result::Result<Vec<u8>, _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_value(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
match res {
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
@@ -227,27 +225,26 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match context
|
||||
let res: std::result::Result<(Vec<u8>, Vec<u8>), _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
WHERE addr=?
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
paramsx![addr.to_string()],
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
match res {
|
||||
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let start = std::time::SystemTime::now();
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
let start = std::time::Instant::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
@@ -258,7 +255,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().unwrap_or_default().as_secs()
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair)
|
||||
}
|
||||
@@ -321,14 +318,14 @@ pub async fn store_self_keypair(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
paramsx![&public_key, &secret_key],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsx![])
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
}
|
||||
@@ -340,7 +337,7 @@ 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];
|
||||
let params = paramsx![addr, is_default, public_key, secret_key, t];
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -616,10 +613,12 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let nrows = || async {
|
||||
ctx1.sql
|
||||
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
let val: i32 = ctx1
|
||||
.sql
|
||||
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
val as usize
|
||||
};
|
||||
assert_eq!(nrows().await, 0);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
|
||||
16
src/lib.rs
16
src/lib.rs
@@ -1,26 +1,15 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing
|
||||
)]
|
||||
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![allow(clippy::match_bool)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
#[macro_use]
|
||||
extern crate smallvec;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
extern crate strum;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
#[macro_use]
|
||||
@@ -48,7 +37,6 @@ pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
mod scheduler;
|
||||
|
||||
473
src/location.rs
473
src/location.rs
@@ -1,5 +1,6 @@
|
||||
//! Location handling
|
||||
|
||||
use async_std::prelude::*;
|
||||
use bitflags::bitflags;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
@@ -37,6 +38,34 @@ impl Location {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Location {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
|
||||
let msg_id = row.try_get::<i32, _>("msg_id")? as u32;
|
||||
let txt: String = row.try_get("txt")?;
|
||||
let marker = if msg_id != 0 && is_marker(&txt) {
|
||||
Some(txt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let loc = Location {
|
||||
location_id: row.try_get::<i32, _>("id")? as u32,
|
||||
latitude: row.try_get("latitude")?,
|
||||
longitude: row.try_get("longitude")?,
|
||||
accuracy: row.try_get("accuracy")?,
|
||||
timestamp: row.try_get("timestamp")?,
|
||||
independent: row.try_get::<i32, _>("independent")? as u32,
|
||||
msg_id,
|
||||
contact_id: row.try_get::<i32, _>("from_id")? as u32,
|
||||
chat_id: row.try_get("chat_id")?,
|
||||
marker,
|
||||
};
|
||||
|
||||
Ok(loc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Kml {
|
||||
pub addr: Option<String>,
|
||||
@@ -197,14 +226,16 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET locations_send_begin=?,
|
||||
locations_send_until=?
|
||||
WHERE id=?
|
||||
"#,
|
||||
paramsx![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
chat_id
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -260,53 +291,60 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) ->
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
|
||||
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
paramsx![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
pub async fn set(
|
||||
context: &Context,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
accuracy: f64,
|
||||
) -> Result<bool, Error> {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let mut continue_streaming = false;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT id FROM chats WHERE locations_send_until>?;")
|
||||
.bind(time())
|
||||
.fetch(&pool);
|
||||
|
||||
if let Ok(chats) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
{
|
||||
for chat_id in chats {
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
]
|
||||
).await {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
continue_streaming = true;
|
||||
}
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id,): (i64,) = row?;
|
||||
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsx![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF as i32
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
continue_streaming = true;
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
}
|
||||
|
||||
continue_streaming
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
|
||||
Ok(continue_streaming)
|
||||
}
|
||||
|
||||
pub async fn get_range(
|
||||
@@ -319,16 +357,21 @@ pub async fn get_range(
|
||||
if timestamp_to == 0 {
|
||||
timestamp_to = time() + 10;
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"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![
|
||||
.query_rows(
|
||||
r#"
|
||||
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;
|
||||
"#,
|
||||
paramsx![
|
||||
if chat_id.is_unset() { 1 } else { 0 },
|
||||
chat_id,
|
||||
if contact_id == 0 { 1 } else { 0 },
|
||||
@@ -336,36 +379,6 @@ pub async fn get_range(
|
||||
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)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -379,7 +392,7 @@ fn is_marker(txt: &str) -> bool {
|
||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
.execute("DELETE FROM locations;", paramsx![])
|
||||
.await?;
|
||||
context.emit_event(Event::LocationChanged(None));
|
||||
Ok(())
|
||||
@@ -393,16 +406,10 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent): (i64, i64, i64) = 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)?;
|
||||
|
||||
Ok((send_begin, send_until, last_sent))
|
||||
})
|
||||
.await?;
|
||||
paramsx![chat_id]
|
||||
).await?;
|
||||
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
@@ -413,40 +420,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 \
|
||||
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)?);
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
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;
|
||||
"#,
|
||||
)
|
||||
.bind(DC_CONTACT_ID_SELF as i32)
|
||||
.bind(locations_send_begin)
|
||||
.bind(locations_last_sent)
|
||||
.bind(DC_CONTACT_ID_SELF as i32)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (location_id, latitude, longitude, accuracy, timestamp): (i32, f64, f64, f64, i64) =
|
||||
row?;
|
||||
let timestamp = get_kml_timestamp(timestamp);
|
||||
|
||||
ret += &format!(
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
||||
timestamp,
|
||||
accuracy,
|
||||
longitude,
|
||||
latitude
|
||||
);
|
||||
location_count += 1;
|
||||
last_added_location_id = location_id as u32;
|
||||
}
|
||||
|
||||
Ok((location_id, latitude, longitude, accuracy, timestamp))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
|
||||
ret += &format!(
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
||||
timestamp,
|
||||
accuracy,
|
||||
longitude,
|
||||
latitude
|
||||
);
|
||||
location_count += 1;
|
||||
last_added_location_id = location_id as u32;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
@@ -488,7 +496,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
paramsv![timestamp, chat_id],
|
||||
paramsx![timestamp, chat_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -503,7 +511,7 @@ pub async fn set_msg_location_id(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
paramsv![location_id, msg_id],
|
||||
paramsx![location_id as i32, msg_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -530,50 +538,51 @@ pub async fn save(
|
||||
accuracy,
|
||||
..
|
||||
} = location;
|
||||
let (loc_id, ts) = context
|
||||
|
||||
let exists: Option<i32> = 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 (?,?,?,?,?,?,?);",
|
||||
)?;
|
||||
.query_value_optional(
|
||||
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
|
||||
paramsx![timestamp, contact_id as i32],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
if independent || exists.is_none() {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
INSERT INTO locations
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent)
|
||||
VALUES (?,?,?,?,?,?,?);
|
||||
"#,
|
||||
paramsx![
|
||||
timestamp,
|
||||
contact_id as i32,
|
||||
chat_id,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
independent,
|
||||
])?;
|
||||
independent
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
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))
|
||||
})
|
||||
.await?;
|
||||
newest_timestamp = ts;
|
||||
newest_location_id = loc_id;
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = context
|
||||
.sql
|
||||
.get_rowid2(
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
contact_id as i32,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
@@ -587,85 +596,75 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
|
||||
);
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"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)?;
|
||||
continue_streaming = true;
|
||||
let pool = match context.sql.get_pool().await {
|
||||
Ok(pool) => pool,
|
||||
Err(err) => {
|
||||
return job::Status::Finished(Err(err.into()));
|
||||
}
|
||||
};
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, locations_send_begin, locations_last_sent
|
||||
FROM chats
|
||||
WHERE locations_send_until>?;
|
||||
"#,
|
||||
)
|
||||
.bind(now)
|
||||
.fetch(&pool);
|
||||
|
||||
if rows.is_ok() {
|
||||
let msgs = context
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id, locations_send_begin, locations_last_sent): (ChatId, i64, i64) = match row {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
warn!(context, "invalid row: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
continue_streaming = true;
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
continue;
|
||||
}
|
||||
let exists = 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)
|
||||
})
|
||||
.exists(
|
||||
r#"
|
||||
SELECT id
|
||||
FROM locations
|
||||
WHERE from_id=?
|
||||
AND timestamp>=?
|
||||
AND timestamp>?
|
||||
AND independent=0
|
||||
ORDER BY timestamp;",
|
||||
"#,
|
||||
paramsx![
|
||||
DC_CONTACT_ID_SELF as i32,
|
||||
locations_send_begin,
|
||||
locations_last_sent,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for (chat_id, mut msg) in msgs.into_iter() {
|
||||
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);
|
||||
|
||||
// TODO: better error handling
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
@@ -676,6 +675,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
if continue_streaming {
|
||||
schedule_maybe_send_locations(context, true).await;
|
||||
}
|
||||
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
@@ -689,13 +689,12 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
|
||||
let chat_id = ChatId::new(job.foreign_id);
|
||||
|
||||
let (send_begin, send_until) = job_try!(
|
||||
let (send_begin, send_until): (i64, i64) = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
paramsx![chat_id],
|
||||
)
|
||||
.await
|
||||
);
|
||||
@@ -707,8 +706,8 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
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],
|
||||
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||
paramsx![chat_id],
|
||||
).await);
|
||||
|
||||
let stock_str = context
|
||||
|
||||
@@ -56,63 +56,54 @@ impl LoginParam {
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
let addr = sql
|
||||
.get_raw_config(context, key)
|
||||
.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!("{}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!("{}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();
|
||||
|
||||
LoginParam {
|
||||
addr,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use deltachat_derive::*;
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
|
||||
@@ -22,7 +22,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, Sqlx)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
@@ -67,7 +67,7 @@ impl Lot {
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
|
||||
621
src/message.rs
621
src/message.rs
@@ -1,12 +1,12 @@
|
||||
//! # Messages and their identifiers
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use async_std::prelude::*;
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
@@ -34,7 +34,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,
|
||||
ToPrimitive,
|
||||
FromPrimitive,
|
||||
Sqlx,
|
||||
)]
|
||||
pub struct MsgId(u32);
|
||||
|
||||
@@ -69,38 +82,18 @@ impl MsgId {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Returns message state.
|
||||
pub async fn get_state(self, context: &Context) -> crate::sql::Result<MessageState> {
|
||||
let result = context
|
||||
.sql
|
||||
.query_get_value_result("SELECT state FROM msgs WHERE id=?", paramsv![self])
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(result)
|
||||
/// Whether the message ID is the special marker1 marker.
|
||||
///
|
||||
/// See the docs of the `dc_get_chat_msgs` C API for details.
|
||||
pub fn is_marker1(self) -> bool {
|
||||
self.0 == DC_MSG_ID_MARKER1
|
||||
}
|
||||
|
||||
/// Returns true if the message needs to be moved from `folder`.
|
||||
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<bool, Error> {
|
||||
if !context.get_config_bool(Config::MvboxMove).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if context.is_mvbox(folder).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => Ok(false),
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
|
||||
}
|
||||
/// Whether the message ID is the special day marker.
|
||||
///
|
||||
/// See the docs of the `dc_get_chat_msgs` C API for details.
|
||||
pub fn is_daymarker(self) -> bool {
|
||||
self.0 == DC_MSG_ID_DAYMARKER
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
@@ -113,7 +106,7 @@ impl MsgId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
|
||||
paramsv![chat_id, self],
|
||||
paramsx![chat_id, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -126,11 +119,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("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsx![self])
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
|
||||
.execute("DELETE FROM msgs WHERE id=?;", paramsx![self])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -144,10 +137,12 @@ impl MsgId {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
paramsv![self],
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET server_folder='', server_uid=0
|
||||
WHERE id=?
|
||||
"#,
|
||||
paramsx![self],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -164,42 +159,16 @@ impl MsgId {
|
||||
|
||||
impl std::fmt::Display for MsgId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Msg#{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<rusqlite::types::ToSqlOutput> {
|
||||
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
|
||||
InvalidMsgId,
|
||||
)));
|
||||
// Would be nice if we could use match here, but no computed values in ranges.
|
||||
if self.0 == DC_MSG_ID_MARKER1 {
|
||||
write!(f, "Msg#Marker1")
|
||||
} else if self.0 == DC_MSG_ID_DAYMARKER {
|
||||
write!(f, "Msg#DayMarker")
|
||||
} else if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||
write!(f, "Msg#UnknownSpecial")
|
||||
} else {
|
||||
write!(f, "Msg#{}", self.0)
|
||||
}
|
||||
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<Self> {
|
||||
// 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,16 +182,7 @@ 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,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum MessengerMessage {
|
||||
@@ -258,8 +218,6 @@ pub struct Message {
|
||||
pub(crate) timestamp_sort: i64,
|
||||
pub(crate) timestamp_sent: i64,
|
||||
pub(crate) timestamp_rcvd: i64,
|
||||
pub(crate) ephemeral_timer: u32,
|
||||
pub(crate) ephemeral_timestamp: i64,
|
||||
pub(crate) text: Option<String>,
|
||||
pub(crate) rfc724_mid: String,
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
@@ -273,6 +231,58 @@ pub struct Message {
|
||||
pub(crate) param: Params,
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Message {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
|
||||
let id = row.try_get("id")?;
|
||||
|
||||
let text;
|
||||
if let Some(buf) = row.try_get_unchecked::<Option<&[u8]>, _>("txt")? {
|
||||
if let Ok(t) = String::from_utf8(buf.to_vec()) {
|
||||
text = t;
|
||||
} else {
|
||||
eprintln!(
|
||||
"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();
|
||||
}
|
||||
|
||||
Ok(Message {
|
||||
id,
|
||||
rfc724_mid: row.try_get::<String, _>("rfc724mid")?,
|
||||
in_reply_to: row.try_get::<Option<String>, _>("mime_in_reply_to")?,
|
||||
server_folder: row.try_get::<Option<String>, _>("server_folder")?,
|
||||
server_uid: row.try_get_unchecked::<i64, _>("server_uid")? as u32,
|
||||
chat_id: row.try_get("chat_id")?,
|
||||
from_id: row.try_get::<i32, _>("from_id")? as u32,
|
||||
to_id: row.try_get::<i32, _>("to_id")? as u32,
|
||||
timestamp_sort: row.try_get_unchecked("timestamp")?,
|
||||
timestamp_sent: row.try_get_unchecked("timestamp_sent")?,
|
||||
timestamp_rcvd: row.try_get_unchecked("timestamp_rcvd")?,
|
||||
viewtype: row.try_get("type")?,
|
||||
state: row.try_get("state")?,
|
||||
is_dc_message: row.try_get("msgrmsg")?,
|
||||
text: Some(text),
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
starred: row.try_get::<i32, _>("starred")? > 0,
|
||||
hidden: row.try_get::<i32, _>("hidden")? > 0,
|
||||
location_id: row.try_get_unchecked::<i64, _>("location")? as u32,
|
||||
chat_blocked: row
|
||||
.try_get::<Option<Blocked>, _>("blocked")?
|
||||
.unwrap_or_default(),
|
||||
error: row.try_get("error")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(viewtype: Viewtype) -> Self {
|
||||
let mut msg = Message::default();
|
||||
@@ -286,7 +296,7 @@ impl Message {
|
||||
!id.is_special(),
|
||||
"Can not load special message IDs from DB."
|
||||
);
|
||||
let msg = context
|
||||
let msg: Message = context
|
||||
.sql
|
||||
.query_row(
|
||||
concat!(
|
||||
@@ -302,8 +312,6 @@ impl Message {
|
||||
" m.timestamp AS timestamp,",
|
||||
" m.timestamp_sent AS timestamp_sent,",
|
||||
" m.timestamp_rcvd AS timestamp_rcvd,",
|
||||
" m.ephemeral_timer AS ephemeral_timer,",
|
||||
" m.ephemeral_timestamp AS ephemeral_timestamp,",
|
||||
" m.type AS type,",
|
||||
" m.state AS state,",
|
||||
" m.error AS error,",
|
||||
@@ -317,58 +325,7 @@ impl Message {
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=?;"
|
||||
),
|
||||
paramsv![id],
|
||||
|row| {
|
||||
let mut msg = Message::default();
|
||||
// msg.id = row.get::<_, AnyMsgId>("id")?;
|
||||
msg.id = row.get("id")?;
|
||||
msg.rfc724_mid = row.get::<_, String>("rfc724mid")?;
|
||||
msg.in_reply_to = row.get::<_, Option<String>>("mime_in_reply_to")?;
|
||||
msg.server_folder = row.get::<_, Option<String>>("server_folder")?;
|
||||
msg.server_uid = row.get("server_uid")?;
|
||||
msg.chat_id = row.get("chat_id")?;
|
||||
msg.from_id = row.get("from_id")?;
|
||||
msg.to_id = row.get("to_id")?;
|
||||
msg.timestamp_sort = row.get("timestamp")?;
|
||||
msg.timestamp_sent = row.get("timestamp_sent")?;
|
||||
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
|
||||
msg.ephemeral_timer = row.get("ephemeral_timer")?;
|
||||
msg.ephemeral_timestamp = row.get("ephemeral_timestamp")?;
|
||||
msg.viewtype = row.get("type")?;
|
||||
msg.state = row.get("state")?;
|
||||
msg.error = row.get("error")?;
|
||||
msg.is_dc_message = row.get("msgrmsg")?;
|
||||
|
||||
let text;
|
||||
if let rusqlite::types::ValueRef::Text(buf) = row.get_raw("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();
|
||||
}
|
||||
msg.text = Some(text);
|
||||
|
||||
msg.param = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
msg.starred = row.get("starred")?;
|
||||
msg.hidden = row.get("hidden")?;
|
||||
msg.location_id = row.get("location")?;
|
||||
msg.chat_blocked = row
|
||||
.get::<_, Option<Blocked>>("blocked")?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(msg)
|
||||
},
|
||||
paramsx![id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -528,14 +485,6 @@ impl Message {
|
||||
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
|
||||
}
|
||||
|
||||
pub fn get_ephemeral_timer(&self) -> u32 {
|
||||
self.ephemeral_timer
|
||||
}
|
||||
|
||||
pub fn get_ephemeral_timestamp(&self) -> i64 {
|
||||
self.ephemeral_timestamp
|
||||
}
|
||||
|
||||
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot {
|
||||
let mut ret = Lot::new();
|
||||
|
||||
@@ -680,7 +629,7 @@ impl Message {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?;",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
paramsx![self.param.to_string(), self.id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -695,10 +644,9 @@ impl Message {
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
ToSql,
|
||||
FromSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum MessageState {
|
||||
@@ -870,29 +818,16 @@ impl Lot {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String, Error> {
|
||||
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();
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let rawtxt: Option<String> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT txt_raw FROM msgs WHERE id=?;",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.await;
|
||||
.query_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsx![msg_id])
|
||||
.await?;
|
||||
|
||||
if rawtxt.is_none() {
|
||||
ret += &format!("Cannot load message {}.", msg_id);
|
||||
return ret;
|
||||
}
|
||||
let rawtxt = rawtxt.unwrap_or_default();
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
|
||||
|
||||
@@ -901,8 +836,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
|
||||
let name = Contact::load_from_db(context, msg.from_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
.unwrap_or_default();
|
||||
.map(|contact| contact.get_name_n_addr())?;
|
||||
|
||||
ret += &format!(" by {}", name);
|
||||
ret += "\n";
|
||||
@@ -917,48 +851,30 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
ret += "\n";
|
||||
}
|
||||
|
||||
if msg.ephemeral_timer != 0 {
|
||||
ret += &format!("Ephemeral timer: {}\n", msg.ephemeral_timer);
|
||||
}
|
||||
|
||||
if msg.ephemeral_timestamp != 0 {
|
||||
ret += &format!(
|
||||
"Expires: {}\n",
|
||||
dc_timestamp_to_str(msg.ephemeral_timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
.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::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
{
|
||||
for (contact_id, ts) in rows {
|
||||
let fts = dc_timestamp_to_str(ts);
|
||||
ret += &format!("Read: {}", fts);
|
||||
let pool = context.sql.get_pool().await?;
|
||||
|
||||
let name = Contact::load_from_db(context, contact_id as u32)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
.unwrap_or_default();
|
||||
let mut rows =
|
||||
sqlx::query_as("SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;")
|
||||
.bind(msg_id)
|
||||
.fetch(&pool);
|
||||
|
||||
ret += &format!(" by {}", name);
|
||||
ret += "\n";
|
||||
}
|
||||
while let Some(row) = rows.next().await {
|
||||
let (contact_id, ts): (i32, i32) = row?;
|
||||
|
||||
let fts = dc_timestamp_to_str(ts as i64);
|
||||
ret += &format!("Read: {}", fts);
|
||||
|
||||
let name = Contact::load_from_db(context, contact_id as u32)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())?;
|
||||
|
||||
ret += &format!(" by {}", name);
|
||||
ret += "\n";
|
||||
}
|
||||
|
||||
ret += &format!("State: {}", msg.state);
|
||||
@@ -1015,7 +931,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)> {
|
||||
@@ -1043,12 +959,12 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT mime_headers FROM msgs WHERE id=?;",
|
||||
paramsv![msg_id],
|
||||
paramsx![msg_id],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
@@ -1087,7 +1003,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM locations WHERE independent = 1 AND id=?;",
|
||||
paramsv![location_id as i32],
|
||||
paramsx![location_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1098,64 +1014,40 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
let msgs = context
|
||||
.sql
|
||||
.with_conn(move |conn| {
|
||||
let mut stmt = conn.prepare_cached(concat!(
|
||||
"SELECT",
|
||||
" m.state AS state,",
|
||||
" c.blocked AS blocked",
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=? AND m.chat_id>9"
|
||||
))?;
|
||||
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids.into_iter() {
|
||||
let query_res = stmt.query_row(paramsv![id], |row| {
|
||||
Ok((
|
||||
row.get::<_, MessageState>("state")?,
|
||||
row.get::<_, Option<Blocked>>("blocked")?
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
});
|
||||
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
|
||||
continue;
|
||||
}
|
||||
let (state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
|
||||
msgs.push((id, state, blocked));
|
||||
}
|
||||
|
||||
Ok(msgs)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut send_event = false;
|
||||
for id in msg_ids.into_iter() {
|
||||
let query_res: Result<Option<(MessageState, Option<Blocked>)>, _> = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT
|
||||
m.state
|
||||
c.blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id = m.chat_id
|
||||
WHERE m.id = ? AND m.chat_id > 9
|
||||
"#,
|
||||
paramsx![id],
|
||||
)
|
||||
.await;
|
||||
|
||||
for (id, curr_state, curr_blocked) in msgs.into_iter() {
|
||||
if let Err(err) = id.start_ephemeral_timer(context).await {
|
||||
error!(
|
||||
context,
|
||||
"Failed to start ephemeral timer for message {}: {}", id, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Ok(Some((state, blocked))) = query_res {
|
||||
let blocked = blocked.unwrap_or_default();
|
||||
if blocked == Blocked::Not {
|
||||
if state == MessageState::InFresh || state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
if curr_blocked == Blocked::Not {
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
send_event = true;
|
||||
}
|
||||
} else if state == MessageState::InFresh {
|
||||
update_msg_state(context, id, MessageState::InNoticed).await;
|
||||
send_event = true;
|
||||
}
|
||||
} else if curr_state == MessageState::InFresh {
|
||||
update_msg_state(context, id, MessageState::InNoticed).await;
|
||||
send_event = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,7 +1066,7 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE id=?;",
|
||||
paramsv![state, msg_id],
|
||||
paramsx![state, msg_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1184,20 +1076,25 @@ pub async fn star_msgs(context: &Context, msg_ids: Vec<MsgId>, star: bool) -> bo
|
||||
if msg_ids.is_empty() {
|
||||
return false;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.with_conn(move |conn| {
|
||||
let mut stmt = conn.prepare("UPDATE msgs SET starred=? WHERE id=?;")?;
|
||||
for msg_id in msg_ids.into_iter() {
|
||||
stmt.execute(paramsv![star as i32, msg_id])?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
|
||||
for msg_id in msg_ids.into_iter() {
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET starred=? WHERE id=?;",
|
||||
paramsx![star as i32, msg_id],
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns a summary text.
|
||||
/// Returns a summary test.
|
||||
pub async fn get_summarytext_by_raw(
|
||||
viewtype: Viewtype,
|
||||
text: Option<impl AsRef<str>>,
|
||||
@@ -1285,12 +1182,9 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> bool {
|
||||
|
||||
let chat_id: Option<ChatId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT chat_id FROM msgs WHERE id=?;",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.await;
|
||||
.query_value("SELECT chat_id FROM msgs WHERE id=?;", paramsx![msg_id])
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
!chat_id.is_trash()
|
||||
@@ -1316,7 +1210,7 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
paramsv![msg.state, error, msg_id],
|
||||
paramsx![msg.state, error, msg_id],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1338,32 +1232,23 @@ pub async fn handle_mdn(
|
||||
rfc724_mid: &str,
|
||||
timestamp_sent: i64,
|
||||
) -> Option<(ChatId, MsgId)> {
|
||||
if from_id <= DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
|
||||
if from_id <= DC_MSG_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = context
|
||||
let res: Result<(MsgId, ChatId, Chattype, MessageState), _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS msg_id,",
|
||||
" c.id AS chat_id,",
|
||||
" c.type AS type,",
|
||||
" m.state AS state",
|
||||
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
" ORDER BY m.id;"
|
||||
),
|
||||
paramsv![rfc724_mid],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, MsgId>("msg_id")?,
|
||||
row.get::<_, ChatId>("chat_id")?,
|
||||
row.get::<_, Chattype>("type")?,
|
||||
row.get::<_, MessageState>("state")?,
|
||||
))
|
||||
},
|
||||
r#"
|
||||
SELECT
|
||||
m.id AS msg_id,
|
||||
c.id AS chat_id,
|
||||
c.type AS type,
|
||||
m.state AS state
|
||||
FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id
|
||||
WHERE rfc724_mid=? AND from_id=1
|
||||
ORDER BY m.id;"#,
|
||||
paramsx![rfc724_mid],
|
||||
)
|
||||
.await;
|
||||
if let Err(ref err) = res {
|
||||
@@ -1381,7 +1266,7 @@ pub async fn handle_mdn(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
|
||||
paramsv![msg_id, from_id as i32,],
|
||||
paramsx![msg_id, from_id as i32,],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -1389,7 +1274,7 @@ pub async fn handle_mdn(
|
||||
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],
|
||||
paramsx![msg_id, from_id as i32, timestamp_sent],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(); // TODO: better error handling
|
||||
@@ -1401,15 +1286,16 @@ pub async fn handle_mdn(
|
||||
read_by_all = true;
|
||||
} else {
|
||||
// send event about new state
|
||||
let ist_cnt = context
|
||||
let ist_cnt: i32 = context
|
||||
.sql
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
|
||||
paramsv![msg_id],
|
||||
paramsx![msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default() as usize;
|
||||
.unwrap_or_default();
|
||||
let ist_cnt = ist_cnt as usize;
|
||||
|
||||
/*
|
||||
Groupsize: Min. MDNs
|
||||
|
||||
@@ -1450,25 +1336,18 @@ pub(crate) async fn handle_ndn(
|
||||
return;
|
||||
}
|
||||
|
||||
let res = context
|
||||
let res: Result<(MsgId, ChatId, Chattype), _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
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")?,
|
||||
))
|
||||
},
|
||||
r#"
|
||||
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
|
||||
"#,
|
||||
paramsx![&failed.rfc724_mid],
|
||||
)
|
||||
.await;
|
||||
if let Err(ref err) = res {
|
||||
@@ -1506,12 +1385,13 @@ pub(crate) async fn handle_ndn(
|
||||
pub async fn get_real_msg_cnt(context: &Context) -> i32 {
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT COUNT(*) \
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
|
||||
WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
.query_value(
|
||||
r#"
|
||||
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;
|
||||
"#,
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1524,17 +1404,17 @@ pub async fn get_real_msg_cnt(context: &Context) -> i32 {
|
||||
}
|
||||
|
||||
pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize {
|
||||
match context
|
||||
let res: Result<i32, _> = context
|
||||
.sql
|
||||
.query_row(
|
||||
"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),
|
||||
.query_value(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE c.blocked=2;"#,
|
||||
paramsx![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
match res {
|
||||
Ok(res) => res as usize,
|
||||
Err(err) => {
|
||||
error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err);
|
||||
@@ -1554,37 +1434,39 @@ pub async fn estimate_deletion_cnt(
|
||||
.0;
|
||||
let threshold_timestamp = time() - seconds;
|
||||
|
||||
let cnt: isize = if from_server {
|
||||
let cnt: i32 = if from_server {
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT COUNT(*)
|
||||
.query_value(
|
||||
r#"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),
|
||||
AND server_uid != 0;"#,
|
||||
paramsx![
|
||||
DC_MSG_ID_LAST_SPECIAL as i32,
|
||||
threshold_timestamp,
|
||||
self_chat_id
|
||||
],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT COUNT(*)
|
||||
.query_value(
|
||||
r#"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,
|
||||
AND chat_id != ? AND hidden = 0;"#,
|
||||
paramsx![
|
||||
DC_MSG_ID_LAST_SPECIAL as i32,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
ChatId::new(DC_CHAT_ID_TRASH)
|
||||
],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -1599,10 +1481,9 @@ pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
|
||||
// check the number of messages with the same rfc724_mid
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_value(
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
|
||||
paramsv![rfc724_mid],
|
||||
|row| row.get(0),
|
||||
paramsx![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1623,22 +1504,15 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let res = context
|
||||
let res: Option<(Option<String>, i32, MsgId)> = 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<String>>(0)?.unwrap_or_default();
|
||||
let server_uid = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
|
||||
Ok((server_folder, server_uid, msg_id))
|
||||
},
|
||||
paramsx![rfc724_mid],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
Ok(res.map(|(a, b, c)| (a.unwrap_or_default(), b as u32, c)))
|
||||
}
|
||||
|
||||
pub async fn update_server_uid(
|
||||
@@ -1650,9 +1524,8 @@ pub async fn update_server_uid(
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? \
|
||||
WHERE rfc724_mid=?",
|
||||
paramsv![server_folder.as_ref(), server_uid, rfc724_mid],
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?",
|
||||
paramsx![server_folder.as_ref(), server_uid as i32, rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use async_std::prelude::*;
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
|
||||
@@ -9,7 +10,6 @@ use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
@@ -88,30 +88,26 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
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(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
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;
|
||||
"#,
|
||||
)
|
||||
.bind(msg.chat_id)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (authname, addr): (String, String) = row?;
|
||||
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
}
|
||||
|
||||
let command = msg.param.get_cmd();
|
||||
|
||||
@@ -126,18 +122,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.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),
|
||||
))
|
||||
},
|
||||
paramsx![msg.id],
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map(|(in_reply_to, references): (String, String)| {
|
||||
(
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
)
|
||||
})?;
|
||||
|
||||
let default_str = context
|
||||
.stock_str(StockMessage::StatusLine)
|
||||
@@ -212,7 +205,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
|
||||
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
|
||||
let self_addr = self
|
||||
.context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -226,7 +219,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.filter(|(_, addr)| addr != &self_addr)
|
||||
{
|
||||
res.push((
|
||||
Peerstate::from_addr(self.context, addr).await,
|
||||
Peerstate::from_addr(self.context, addr).await.ok(),
|
||||
addr.as_str(),
|
||||
));
|
||||
}
|
||||
@@ -499,16 +492,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(self.context).await?;
|
||||
|
||||
let subject = if subject_str
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
|
||||
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
|
||||
// but we do not want to encode all subjects just because they contain a space.
|
||||
{
|
||||
subject_str
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
let subject = encode_words(&subject_str);
|
||||
|
||||
let mut message = match self.loaded {
|
||||
Loaded::Message { .. } => {
|
||||
@@ -536,14 +520,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(self.context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
protected_headers.push(Header::new(
|
||||
"Ephemeral-Timer".to_string(),
|
||||
duration.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// we could also store the message-id in the protected headers
|
||||
// which would probably help to survive providers like
|
||||
// Outlook.com or hotmail which mangle the Message-ID.
|
||||
@@ -794,26 +770,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
"location-streaming-enabled".into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::EphemeralTimerChanged => {
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
// See https://tools.ietf.org/html/rfc3834
|
||||
//
|
||||
// Adding this header without encryption leaks some
|
||||
// information about the message contents, but it can
|
||||
// already be easily guessed from message timing and size.
|
||||
unprotected_headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-generated".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
unprotected_headers
|
||||
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
@@ -46,14 +46,7 @@ pub struct MimeMessage {
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Set of valid signature fingerprints if a message is an
|
||||
/// Autocrypt encrypted and signed message.
|
||||
///
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
pub gossipped_addr: HashSet<String>,
|
||||
pub is_forwarded: bool,
|
||||
pub is_system_message: SystemMessage,
|
||||
@@ -71,7 +64,7 @@ pub(crate) enum AvatarAction {
|
||||
Change(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[repr(i32)]
|
||||
pub enum SystemMessage {
|
||||
Unknown = 0,
|
||||
@@ -83,9 +76,6 @@ pub enum SystemMessage {
|
||||
SecurejoinMessage = 7,
|
||||
LocationStreamingEnabled = 8,
|
||||
LocationOnly = 9,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -224,8 +214,6 @@ impl MimeMessage {
|
||||
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "location-streaming-enabled" {
|
||||
self.is_system_message = SystemMessage::LocationStreamingEnabled;
|
||||
} else if value == "ephemeral-timer-changed" {
|
||||
self.is_system_message = SystemMessage::EphemeralTimerChanged;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -246,7 +234,6 @@ impl MimeMessage {
|
||||
///
|
||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = {
|
||||
@@ -280,21 +267,22 @@ impl MimeMessage {
|
||||
fn parse_attachments(&mut self) {
|
||||
// Attachment messages should be squashed into a single part
|
||||
// before calling this function.
|
||||
if self.parts.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut part) = self.parts.pop() {
|
||||
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
|
||||
part.typ = Viewtype::Voice;
|
||||
if self.parts.len() == 1 {
|
||||
if self.parts[0].typ == Viewtype::Audio
|
||||
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
|
||||
{
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Voice;
|
||||
}
|
||||
if part.typ == Viewtype::Image {
|
||||
if self.parts[0].typ == Viewtype::Image {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
part.typ = Viewtype::Sticker;
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Sticker;
|
||||
}
|
||||
}
|
||||
}
|
||||
let part = &self.parts[0];
|
||||
if part.typ == Viewtype::Audio
|
||||
|| part.typ == Viewtype::Voice
|
||||
|| part.typ == Viewtype::Video
|
||||
@@ -302,12 +290,11 @@ impl MimeMessage {
|
||||
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
|
||||
let duration_ms = field_0.parse().unwrap_or_default();
|
||||
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
|
||||
part.param.set_int(Param::Duration, duration_ms);
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.param.set_int(Param::Duration, duration_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,11 +316,12 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
if prepend_subject {
|
||||
let subj = subject
|
||||
.find('[')
|
||||
.and_then(|n| subject.get(..n))
|
||||
.unwrap_or(subject)
|
||||
.trim();
|
||||
let subj = if let Some(n) = subject.find('[') {
|
||||
&subject[0..n]
|
||||
} else {
|
||||
subject
|
||||
}
|
||||
.trim();
|
||||
|
||||
if !subj.is_empty() {
|
||||
for part in self.parts.iter_mut() {
|
||||
@@ -391,7 +379,8 @@ impl MimeMessage {
|
||||
Some(AvatarAction::Delete)
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while let Some(part) = self.parts.get_mut(i) {
|
||||
while i != self.parts.len() {
|
||||
let part = &mut self.parts[i];
|
||||
if let Some(part_filename) = &part.org_filename {
|
||||
if part_filename == &header_value {
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
@@ -408,11 +397,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the message was encrypted as defined in
|
||||
/// Autocrypt standard.
|
||||
///
|
||||
/// This means the message was both encrypted and signed with a
|
||||
/// valid signature.
|
||||
pub fn was_encrypted(&self) -> bool {
|
||||
!self.signatures.is_empty()
|
||||
}
|
||||
@@ -781,11 +765,16 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
if self.parts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let part = &mut self.parts[0];
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
|
||||
assert_eq!(self.parts.len(), 1);
|
||||
}
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
@@ -836,11 +825,7 @@ impl MimeMessage {
|
||||
report: &mailparse::ParsedMail<'_>,
|
||||
) -> Result<Option<Report>> {
|
||||
// parse as mailheaders
|
||||
let report_body = if let Some(subpart) = report.subparts.get(1) {
|
||||
subpart.get_body_raw()?
|
||||
} else {
|
||||
bail!("Report does not have second MIME part");
|
||||
};
|
||||
let report_body = report.subparts[1].get_body_raw()?;
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
@@ -918,7 +903,6 @@ impl MimeMessage {
|
||||
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
|
||||
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
|
||||
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
|
||||
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
@@ -977,10 +961,11 @@ impl MimeMessage {
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map(|p| {
|
||||
let msg = &p.msg;
|
||||
msg.find("\n--- ")
|
||||
.and_then(|footer_start| msg.get(..footer_start))
|
||||
.unwrap_or(msg)
|
||||
.trim()
|
||||
match msg.find("\n--- ") {
|
||||
Some(footer_start) => &msg[..footer_start],
|
||||
None => msg,
|
||||
}
|
||||
.trim()
|
||||
});
|
||||
message::handle_ndn(context, failure_report, error).await
|
||||
}
|
||||
@@ -1004,12 +989,12 @@ async fn update_gossip_peerstates(
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await.ok();
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_gossip(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(context, header, message_time);
|
||||
let p = Peerstate::from_gossip(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
@@ -1046,7 +1031,6 @@ pub(crate) struct FailureReport {
|
||||
pub failed_recipient: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
|
||||
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||
let mut msgids = Vec::new();
|
||||
|
||||
@@ -95,10 +95,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
|
||||
// read generated token
|
||||
if !regenerate && !is_expired(context).await {
|
||||
let access_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_access_token")
|
||||
.await;
|
||||
let access_token = context.sql.get_raw_config("oauth2_access_token").await;
|
||||
if access_token.is_some() {
|
||||
// success
|
||||
return access_token;
|
||||
@@ -106,13 +103,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
}
|
||||
|
||||
// generate new token: build & call auth url
|
||||
let refresh_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token")
|
||||
.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")
|
||||
.get_raw_config("oauth2_refresh_token_for")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into());
|
||||
|
||||
@@ -122,7 +116,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_pending_redirect_uri")
|
||||
.get_raw_config("oauth2_pending_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.init_token,
|
||||
@@ -136,7 +130,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_redirect_uri")
|
||||
.get_raw_config("oauth2_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.refresh_token,
|
||||
@@ -360,7 +354,7 @@ impl Oauth2 {
|
||||
async fn is_expired(context: &Context) -> bool {
|
||||
let expire_timestamp = context
|
||||
.sql
|
||||
.get_raw_config_int64(context, "oauth2_timestamp_expires")
|
||||
.get_raw_config_int64("oauth2_timestamp_expires")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ impl str::FromStr for Params {
|
||||
let key = key.unwrap_or_default().trim();
|
||||
let value = value.unwrap_or_default().trim();
|
||||
|
||||
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
|
||||
if let Some(key) = Param::from_u8(key.as_bytes()[0]) {
|
||||
inner.insert(key, value.to_string());
|
||||
} else {
|
||||
bail!("Unknown key: {}", key);
|
||||
|
||||
251
src/peerstate.rs
251
src/peerstate.rs
@@ -1,8 +1,8 @@
|
||||
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::context::Context;
|
||||
@@ -24,8 +24,8 @@ pub enum PeerstateVerifiedStatus {
|
||||
}
|
||||
|
||||
/// Peerstate represents the state of an Autocrypt peer.
|
||||
pub struct Peerstate<'a> {
|
||||
pub context: &'a Context,
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Peerstate {
|
||||
pub addr: String,
|
||||
pub last_seen: i64,
|
||||
pub last_seen_autocrypt: i64,
|
||||
@@ -41,43 +41,49 @@ pub struct Peerstate<'a> {
|
||||
pub degrade_event: Option<DegradeEvent>,
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Peerstate<'a> {
|
||||
fn eq(&self, other: &Peerstate) -> bool {
|
||||
self.addr == other.addr
|
||||
&& self.last_seen == other.last_seen
|
||||
&& self.last_seen_autocrypt == other.last_seen_autocrypt
|
||||
&& self.prefer_encrypt == other.prefer_encrypt
|
||||
&& self.public_key == other.public_key
|
||||
&& self.public_key_fingerprint == other.public_key_fingerprint
|
||||
&& self.gossip_key == other.gossip_key
|
||||
&& self.gossip_timestamp == other.gossip_timestamp
|
||||
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
|
||||
&& self.verified_key == other.verified_key
|
||||
&& self.verified_key_fingerprint == other.verified_key_fingerprint
|
||||
&& self.to_save == other.to_save
|
||||
&& self.degrade_event == other.degrade_event
|
||||
}
|
||||
}
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Peerstate {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
|
||||
impl<'a> Eq for Peerstate<'a> {}
|
||||
let mut res = Self::new(row.try_get("addr")?);
|
||||
|
||||
impl<'a> fmt::Debug for Peerstate<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Peerstate")
|
||||
.field("addr", &self.addr)
|
||||
.field("last_seen", &self.last_seen)
|
||||
.field("last_seen_autocrypt", &self.last_seen_autocrypt)
|
||||
.field("prefer_encrypt", &self.prefer_encrypt)
|
||||
.field("public_key", &self.public_key)
|
||||
.field("public_key_fingerprint", &self.public_key_fingerprint)
|
||||
.field("gossip_key", &self.gossip_key)
|
||||
.field("gossip_timestamp", &self.gossip_timestamp)
|
||||
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
|
||||
.field("verified_key", &self.verified_key)
|
||||
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
|
||||
.field("to_save", &self.to_save)
|
||||
.field("degrade_event", &self.degrade_event)
|
||||
.finish()
|
||||
res.last_seen = row.try_get_unchecked("last_seen")?;
|
||||
res.last_seen_autocrypt = row.try_get_unchecked("last_seen_autocrypt")?;
|
||||
res.prefer_encrypt = row.try_get("prefer_encrypted")?;
|
||||
res.gossip_timestamp = row.try_get_unchecked("gossip_timestamp")?;
|
||||
|
||||
res.public_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("public_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.gossip_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("gossip_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.verified_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("verified_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.public_key = row
|
||||
.try_get::<Option<&[u8]>, _>("public_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.gossip_key = row
|
||||
.try_get::<Option<&[u8]>, _>("gossip_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.verified_key = row
|
||||
.try_get::<Option<&[u8]>, _>("verified_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +104,9 @@ pub enum DegradeEvent {
|
||||
FingerprintChanged = 0x02,
|
||||
}
|
||||
|
||||
impl<'a> Peerstate<'a> {
|
||||
pub fn new(context: &'a Context, addr: String) -> Self {
|
||||
impl Peerstate {
|
||||
pub fn new(addr: String) -> Self {
|
||||
Peerstate {
|
||||
context,
|
||||
addr,
|
||||
last_seen: 0,
|
||||
last_seen_autocrypt: 0,
|
||||
@@ -118,8 +123,8 @@ impl<'a> Peerstate<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, header.addr.clone());
|
||||
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(header.addr.clone());
|
||||
|
||||
res.last_seen = message_time;
|
||||
res.last_seen_autocrypt = message_time;
|
||||
@@ -131,8 +136,8 @@ impl<'a> Peerstate<'a> {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, gossip_header.addr.clone());
|
||||
pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(gossip_header.addr.clone());
|
||||
|
||||
res.gossip_timestamp = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
@@ -142,76 +147,53 @@ impl<'a> Peerstate<'a> {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
|
||||
Self::from_stmt(context, query, paramsv![addr]).await
|
||||
pub async fn from_addr(context: &Context, addr: &str) -> Result<Peerstate> {
|
||||
let query = r#"
|
||||
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, paramsx![addr]).await
|
||||
}
|
||||
|
||||
pub async fn from_fingerprint(
|
||||
context: &'a Context,
|
||||
_sql: &Sql,
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
) -> Option<Peerstate<'a>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE 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
|
||||
) -> Result<Peerstate> {
|
||||
let query = r#"
|
||||
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 fingerprint = fingerprint.hex();
|
||||
Self::from_stmt(
|
||||
context,
|
||||
query,
|
||||
paramsx![&fingerprint, &fingerprint, &fingerprint],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn from_stmt(
|
||||
context: &'a Context,
|
||||
query: &str,
|
||||
params: Vec<&dyn crate::ToSql>,
|
||||
) -> Option<Peerstate<'a>> {
|
||||
context
|
||||
.sql
|
||||
.query_row(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
|
||||
*/
|
||||
let mut res = Self::new(context, row.get(0)?);
|
||||
async fn from_stmt<'a, P: sqlx::IntoArguments<'a, sqlx::sqlite::Sqlite> + 'a>(
|
||||
context: &Context,
|
||||
query: &'a str,
|
||||
params: P,
|
||||
) -> Result<Peerstate> {
|
||||
/* 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 peerstate = context.sql.query_row(query, params).await?;
|
||||
|
||||
res.last_seen = row.get(1)?;
|
||||
res.last_seen_autocrypt = row.get(2)?;
|
||||
res.prefer_encrypt = EncryptPreference::from_i32(row.get(3)?).unwrap_or_default();
|
||||
res.gossip_timestamp = row.get(5)?;
|
||||
|
||||
res.public_key_fingerprint = row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.gossip_key_fingerprint = row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.verified_key_fingerprint = row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.public_key = row
|
||||
.get(4)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.gossip_key = row
|
||||
.get(6)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.verified_key = row
|
||||
.get(9)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
Ok(peerstate)
|
||||
}
|
||||
|
||||
pub fn recalc_fingerprint(&mut self) {
|
||||
@@ -403,19 +385,21 @@ impl<'a> Peerstate<'a> {
|
||||
if create {
|
||||
sql.execute(
|
||||
"INSERT INTO acpeerstates (addr) VALUES(?);",
|
||||
paramsv![self.addr],
|
||||
paramsx![&self.addr],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if self.to_save == Some(ToSave::All) || create {
|
||||
sql.execute(
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
|
||||
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
|
||||
verified_key=?, verified_key_fingerprint=? \
|
||||
WHERE addr=?;",
|
||||
paramsv![
|
||||
r#"
|
||||
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=?;
|
||||
"#,
|
||||
paramsx![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
@@ -426,18 +410,17 @@ impl<'a> Peerstate<'a> {
|
||||
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,
|
||||
&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![
|
||||
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? WHERE addr=?;",
|
||||
paramsx![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.gossip_timestamp,
|
||||
self.addr
|
||||
&self.addr
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -455,18 +438,11 @@ impl<'a> Peerstate<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::key::FingerprintError> for rusqlite::Error {
|
||||
fn from(_source: crate::key::FingerprintError) -> Self {
|
||||
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
@@ -476,7 +452,6 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -504,10 +479,9 @@ mod tests {
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
let peerstate_new2 =
|
||||
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
assert_eq!(peerstate, peerstate_new2);
|
||||
}
|
||||
|
||||
@@ -518,7 +492,6 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -552,7 +525,6 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -581,11 +553,4 @@ mod tests {
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
}
|
||||
|
||||
// TODO: don't copy this from stress.rs
|
||||
#[allow(dead_code)]
|
||||
struct TestContext {
|
||||
ctx: Context,
|
||||
dir: TempDir,
|
||||
}
|
||||
}
|
||||
|
||||
12
src/pgp.rs
12
src/pgp.rs
@@ -365,13 +365,11 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
|
||||
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
if let Some(msg) = msgs.first() {
|
||||
match msg.get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
} else {
|
||||
bail!("No valid messages found")
|
||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||
|
||||
match msgs[0].get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
19
src/qr.rs
19
src/qr.rs
@@ -68,7 +68,6 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
|
||||
|
||||
@@ -139,10 +138,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let mut lot = Lot::new();
|
||||
|
||||
// retrieve known state for this fingerprint
|
||||
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
|
||||
let peerstate = Peerstate::from_fingerprint(context, &fingerprint).await;
|
||||
|
||||
if invitenumber.is_none() || auth.is_none() {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Ok(peerstate) = peerstate {
|
||||
lot.state = LotState::QrFprOk;
|
||||
|
||||
lot.id = Contact::add_or_lookup(
|
||||
@@ -188,7 +187,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
}
|
||||
|
||||
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn decode_account(_context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
@@ -219,7 +217,6 @@ struct CreateAccountResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
@@ -243,7 +240,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[MAILTO_SCHEME.len()..];
|
||||
|
||||
@@ -265,7 +261,6 @@ async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
/// Extract address for the smtp scheme.
|
||||
///
|
||||
/// Scheme: `SMTP:addr...:subject...:body...`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[SMTP_SCHEME.len()..];
|
||||
|
||||
@@ -288,7 +283,6 @@ async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
|
||||
///
|
||||
/// There may or may not be linebreaks after the fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
|
||||
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
|
||||
// we ignore this case.
|
||||
@@ -322,15 +316,14 @@ lazy_static! {
|
||||
/// Extract address for the matmsg scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Lot {
|
||||
let name = VCARD_NAME_RE
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
let first_name = caps.get(2)?.as_str().trim();
|
||||
.map(|caps| {
|
||||
let last_name = &caps[1];
|
||||
let first_name = &caps[2];
|
||||
|
||||
Some(format!("{} {}", first_name, last_name))
|
||||
format!("{} {}", first_name.trim(), last_name.trim())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::indexing_slicing)]
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{channel, Receiver, Sender};
|
||||
use async_std::task;
|
||||
@@ -74,13 +76,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
}
|
||||
None => {
|
||||
jobs_loaded = 0;
|
||||
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = connection.maybe_close_folder(&ctx).await {
|
||||
warn!(ctx, "failed to close folder: {:?}", err);
|
||||
}
|
||||
|
||||
info = if ctx.get_config_bool(Config::InboxWatch).await {
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
@@ -127,7 +122,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
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);
|
||||
error!(ctx, "imap connection failed: {}", err);
|
||||
return connection.fake_idle(&ctx, None).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,81 +152,69 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
ongoing_allocated: bool,
|
||||
join_vg: bool,
|
||||
) -> ChatId {
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.expects = 0;
|
||||
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
|
||||
if join_vg {
|
||||
chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or((ChatId::new(0), false, Blocked::Not))
|
||||
.0
|
||||
} else {
|
||||
contact_chat_id
|
||||
}
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
bob.qr_scan = None;
|
||||
|
||||
if ongoing_allocated {
|
||||
context.free_ongoing().await;
|
||||
}
|
||||
ret_chat_id
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
|
||||
/// See the ffi-documentation for more details.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
if context.alloc_ongoing().await.is_err() {
|
||||
return cleanup(&context, ChatId::new(0), false, false).await;
|
||||
if let Err(err) = context.alloc_ongoing().await {
|
||||
error!(context, "SecureJoin Error: {}", err);
|
||||
return ChatId::new(0);
|
||||
}
|
||||
|
||||
securejoin(context, qr).await
|
||||
match securejoin(context, qr).await {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
error!(context, "SecureJoin Error: {}", err);
|
||||
ChatId::new(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
========================================================*/
|
||||
/// Bob - the joiner's side
|
||||
/// Step 2 in "Setup verified contact" protocol
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error> {
|
||||
struct DropGuard<'a> {
|
||||
context: &'a Context,
|
||||
}
|
||||
|
||||
let mut contact_chat_id = ChatId::new(0);
|
||||
let mut join_vg: bool = false;
|
||||
impl Drop for DropGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
async_std::task::block_on(async {
|
||||
let mut bob = self.context.bob.write().await;
|
||||
bob.expects = 0;
|
||||
bob.qr_scan = None;
|
||||
self.context.free_ongoing().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
let _guard = DropGuard { context: &context };
|
||||
|
||||
info!(context, "Requesting secure-join ...");
|
||||
ensure_secret_key_exists(context).await.ok();
|
||||
let qr_scan = check_qr(context, &qr).await;
|
||||
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
|
||||
{
|
||||
error!(context, "Unknown QR code.",);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
bail!("Unknown QR code.");
|
||||
}
|
||||
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
let contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
Ok(chat_id) => chat_id,
|
||||
Err(_) => {
|
||||
error!(context, "Unknown contact.");
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
bail!("Unknown contact.");
|
||||
}
|
||||
};
|
||||
|
||||
if context.shall_stop_ongoing().await {
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
bail!("Interrupted");
|
||||
}
|
||||
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
|
||||
let join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
{
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.status = 0;
|
||||
bob.qr_scan = Some(qr_scan);
|
||||
}
|
||||
if fingerprint_equals_sender(
|
||||
|
||||
let fp_equals_sender = fingerprint_equals_sender(
|
||||
context,
|
||||
context
|
||||
.bob
|
||||
@@ -240,21 +228,22 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
.unwrap(),
|
||||
contact_chat_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await?;
|
||||
|
||||
if fp_equals_sender {
|
||||
// the scanned fingerprint matches Alice's key,
|
||||
// we can proceed to step 4b) directly and save two mails
|
||||
info!(context, "Taking protocol shortcut.");
|
||||
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
||||
joiner_progress!(
|
||||
context,
|
||||
chat_id_2_contact_id(context, contact_chat_id).await,
|
||||
chat_id_2_contact_id(context, contact_chat_id).await?,
|
||||
400
|
||||
);
|
||||
let own_fingerprint = get_self_fingerprint(context).await;
|
||||
|
||||
// Bob -> Alice
|
||||
if let Err(err) = send_handshake_msg(
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg {
|
||||
@@ -270,16 +259,12 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
"".to_string()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
.await?;
|
||||
} else {
|
||||
context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
|
||||
|
||||
// Bob -> Alice
|
||||
if let Err(err) = send_handshake_msg(
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg { "vg-request" } else { "vc-request" },
|
||||
@@ -287,11 +272,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
None,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
.await?;
|
||||
}
|
||||
|
||||
if join_vg {
|
||||
@@ -299,12 +280,23 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
while !context.shall_stop_ongoing().await {
|
||||
async_std::task::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg).await
|
||||
let bob = context.bob.read().await;
|
||||
if bob.status == DC_BOB_SUCCESS {
|
||||
let id = chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
Ok(id)
|
||||
} else {
|
||||
bail!("Failed to join");
|
||||
}
|
||||
} else {
|
||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||
// the verification runs in background
|
||||
context.free_ongoing().await;
|
||||
contact_chat_id
|
||||
Ok(contact_chat_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,11 +343,12 @@ async fn send_handshake_msg(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
contact_id
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
|
||||
if contacts.len() == 1 {
|
||||
Ok(contacts[0])
|
||||
} else {
|
||||
0
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,20 +356,24 @@ 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[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
) -> Result<bool, Error> {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
|
||||
|
||||
if contacts.len() == 1 {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
|
||||
if let Ok(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HandshakeError {
|
||||
#[error("Can not be called with special contact ID")]
|
||||
@@ -397,6 +394,8 @@ pub(crate) enum HandshakeError {
|
||||
MsgSendFailed(#[source] Error),
|
||||
#[error("Failed to parse fingerprint")]
|
||||
BadFingerprint(#[from] crate::key::FingerprintError),
|
||||
#[error("{0}")]
|
||||
Other(#[from] Error),
|
||||
}
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
@@ -423,7 +422,6 @@ pub(crate) enum HandshakeMessage {
|
||||
/// When handle_securejoin_handshake() is called,
|
||||
/// the message is not yet filed in the database;
|
||||
/// this is done by receive_imf() later on as needed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -531,20 +529,20 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Not encrypted."
|
||||
},
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id)
|
||||
.await
|
||||
.await?
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
@@ -587,7 +585,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
@@ -597,16 +595,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.",);
|
||||
@@ -619,13 +617,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() {
|
||||
@@ -634,12 +632,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, Event::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
@@ -742,7 +740,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Contact confirm message not encrypted.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
context.bob.write().await.status = 0;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
@@ -756,7 +754,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await;
|
||||
@@ -774,7 +772,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
context.bob.write().await.expects = 0;
|
||||
|
||||
// Bob -> Alice
|
||||
@@ -899,7 +897,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)
|
||||
@@ -911,7 +909,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);
|
||||
}
|
||||
};
|
||||
@@ -921,7 +919,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" {
|
||||
@@ -934,8 +932,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<(), HandshakeError> {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
|
||||
let addr = if let Ok(ref contact) = contact {
|
||||
@@ -948,14 +949,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
|
||||
.await;
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
|
||||
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<(), HandshakeError> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
let msg = context
|
||||
.stock_string_repl_str(
|
||||
@@ -970,12 +973,11 @@ async fn could_not_establish_secure_connection(
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, &msg).await;
|
||||
error!(context, "{} ({})", &msg, details);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
|
||||
if let Some(ref mut peerstate) =
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await
|
||||
{
|
||||
if let Ok(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await {
|
||||
if peerstate.set_verified(
|
||||
PeerstateKeyType::PublicKey,
|
||||
fingerprint,
|
||||
@@ -1008,47 +1010,42 @@ fn encrypted_and_signed(
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if let Some(expected_fingerprint) = expected_fingerprint {
|
||||
if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
} else if mimeparser.signatures.is_empty() {
|
||||
warn!(context, "Message not signed.",);
|
||||
false
|
||||
} else if expected_fingerprint.is_none() {
|
||||
warn!(context, "Fingerprint for comparison missing.");
|
||||
false
|
||||
} else if !mimeparser
|
||||
.signatures
|
||||
.contains(expected_fingerprint.unwrap())
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.unwrap(),
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_degrade_event(
|
||||
context: &Context,
|
||||
peerstate: &Peerstate<'_>,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<(), Error> {
|
||||
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
|
||||
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
|
||||
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
|
||||
// with things they cannot fix, so the user is just kicked from the verified group
|
||||
// (and he will know this and can fix this)
|
||||
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
|
||||
let contact_id: i32 = match context
|
||||
let contact_id: i32 = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
paramsv![peerstate.addr],
|
||||
paramsx![&peerstate.addr],
|
||||
)
|
||||
.await
|
||||
{
|
||||
None => bail!(
|
||||
"contact with peerstate.addr {:?} not found",
|
||||
&peerstate.addr
|
||||
),
|
||||
Some(contact_id) => contact_id,
|
||||
};
|
||||
.await?;
|
||||
|
||||
if contact_id > 0 {
|
||||
let (contact_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
// but for non-delta-compatibility, that seems to be better.
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
if text.starts_with("--") {
|
||||
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
@@ -42,7 +41,6 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
|
||||
/// Remove nonstandard footer and a boolean indicating whether such
|
||||
/// footer was removed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
if line == "--"
|
||||
@@ -109,7 +107,6 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
for (l, line) in lines.iter().enumerate().rev() {
|
||||
@@ -135,7 +132,6 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
let mut has_quoted_headline = false;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! # SMTP transport module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
pub mod send;
|
||||
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_smtp::smtp::client::net::*;
|
||||
use async_smtp::*;
|
||||
@@ -55,7 +57,7 @@ pub(crate) struct Smtp {
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
last_success: Option<SystemTime>,
|
||||
last_success: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
@@ -76,11 +78,7 @@ impl Smtp {
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub async fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
SystemTime::now()
|
||||
.duration_since(last_success)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
> 60
|
||||
Instant::now().duration_since(last_success).as_secs() > 60
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -183,7 +181,7 @@ impl Smtp {
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", domain, port),
|
||||
err.to_string(),
|
||||
format!("{}, ({:?})", err.to_string(), err),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -192,7 +190,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(SystemTime::now());
|
||||
self.last_success = Some(Instant::now());
|
||||
|
||||
context.emit_event(Event::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Smtp {
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
)));
|
||||
self.last_success = Some(std::time::SystemTime::now());
|
||||
self.last_success = Some(std::time::Instant::now());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
1367
src/sql.rs
1367
src/sql.rs
File diff suppressed because it is too large
Load Diff
13
src/sql/macros.rs
Normal file
13
src/sql/macros.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#[macro_export]
|
||||
macro_rules! paramsx {
|
||||
() => {
|
||||
sqlx::sqlite::SqliteArguments::default()
|
||||
};
|
||||
($($param:expr),+ $(,)?) => {{
|
||||
use sqlx::Arguments;
|
||||
|
||||
let mut args = sqlx::sqlite::SqliteArguments::default();
|
||||
$(args.add($param);)+
|
||||
args
|
||||
}};
|
||||
}
|
||||
378
src/sql/migrations.rs
Normal file
378
src/sql/migrations.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use super::{Error, Result, Sql};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
|
||||
/// Executes all migrations required to get from the passed in `dbversion` to the latest.
|
||||
pub async fn run(
|
||||
context: &Context,
|
||||
sql: &Sql,
|
||||
dbversion: i32,
|
||||
exists_before_update: bool,
|
||||
) -> Result<()> {
|
||||
let migrate = |version: i32, stmt: &'static str| async move {
|
||||
if dbversion < version {
|
||||
info!(context, "[migration] v{}", version);
|
||||
|
||||
sql.execute_batch(stmt).await?;
|
||||
sql.set_raw_config_int(context, "dbversion", version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
};
|
||||
|
||||
migrate(
|
||||
0,
|
||||
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 '');
|
||||
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 '');
|
||||
CREATE INDEX chats_index1 ON chats (grpid);
|
||||
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
|
||||
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
|
||||
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 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 '');
|
||||
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);
|
||||
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 '');
|
||||
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
1,
|
||||
r#"
|
||||
CREATE TABLE leftgrps (
|
||||
id INTEGER PRIMARY KEY,
|
||||
grpid TEXT DEFAULT '');
|
||||
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
2,
|
||||
r#"
|
||||
ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
7,
|
||||
r#"
|
||||
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);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
10,
|
||||
r#"
|
||||
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);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
12,
|
||||
r#"
|
||||
CREATE TABLE msgs_mdns (
|
||||
msg_id INTEGER,
|
||||
contact_id INTEGER);
|
||||
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
17,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
18,
|
||||
r#"
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_key;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// chat.id=1 and chat.id=2 are the old deaddrops,
|
||||
// the current ones are defined by chats.blocked=2
|
||||
migrate(
|
||||
27,
|
||||
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;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
34,
|
||||
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);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
39,
|
||||
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);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
40,
|
||||
r#"
|
||||
ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
44,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_headers TEXT;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
46,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
|
||||
ALTER TABLE msgs ADD COLUMN mime_references TEXT;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
47,
|
||||
r#"
|
||||
ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// NOTE: move_state is not used anymore
|
||||
migrate(
|
||||
48,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
49,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.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?;
|
||||
}
|
||||
|
||||
// the messages containing _only_ locations
|
||||
// are also added to the database as _hidden_.
|
||||
migrate(
|
||||
53,
|
||||
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);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
54,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index6 ON msgs (location_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
55,
|
||||
r#"
|
||||
ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
59,
|
||||
r#"
|
||||
CREATE TABLE devmsglabels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT,
|
||||
msg_id INTEGER DEFAULT 0);
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// records in the devmsglabels are kept when the message is deleted.
|
||||
// so, msg_id may or may not exist.
|
||||
if dbversion < 59 && exists_before_update && sql.get_raw_config_int("bcc_self").await.is_none()
|
||||
{
|
||||
sql.set_raw_config_int(context, "bcc_self", 1).await?;
|
||||
}
|
||||
|
||||
migrate(
|
||||
60,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
61,
|
||||
r#"
|
||||
ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
62,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
63,
|
||||
r#"
|
||||
UPDATE chats SET grpid='' WHERE type=100;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
64,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
706
src/sql/mod.rs
Normal file
706
src/sql/mod.rs
Normal file
@@ -0,0 +1,706 @@
|
||||
//! # SQLite wrapper
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::RwLock;
|
||||
use sqlx::sqlite::*;
|
||||
|
||||
use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod migrations;
|
||||
|
||||
pub use macros::*;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum 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] crate::error::Error),
|
||||
#[error("{0}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
#[error("{0}: {1}")]
|
||||
SqlxWithContext(String, #[source] sqlx::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
#[derive(Debug)]
|
||||
pub struct Sql {
|
||||
xpool: RwLock<Option<sqlx::SqlitePool>>,
|
||||
}
|
||||
|
||||
impl Default for Sql {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
xpool: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
pub fn new() -> Sql {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns `true` if there is a working sqlite connection, `false` otherwise.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
let pool = self.xpool.read().await;
|
||||
pool.is_some() && !pool.as_ref().unwrap().is_closed()
|
||||
}
|
||||
|
||||
/// Shuts down all sqlite connections.
|
||||
pub async fn close(&self) {
|
||||
if let Some(pool) = self.xpool.write().await.take() {
|
||||
pool.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open<T: AsRef<Path>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
dbfile: T,
|
||||
readonly: bool,
|
||||
) -> crate::error::Result<()> {
|
||||
if let Err(err) = open(context, self, dbfile, readonly).await {
|
||||
return match err.downcast_ref::<Error>() {
|
||||
Some(Error::SqlAlreadyOpen) => Err(err),
|
||||
_ => {
|
||||
self.close().await;
|
||||
Err(err)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a single query.
|
||||
pub async fn execute<'a, P>(&self, statement: &'a str, params: P) -> Result<usize>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let count = sqlx::query_with(statement, params).execute(xpool).await?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
/// Execute a list of statements, without any bindings
|
||||
pub async fn execute_batch(&self, statement: &str) -> Result<()> {
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
sqlx::query(statement.as_ref()).execute(xpool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_pool(&self) -> Result<SqlitePool> {
|
||||
let lock = self.xpool.read().await;
|
||||
lock.as_ref().cloned().ok_or_else(|| Error::SqlNoConnection)
|
||||
}
|
||||
|
||||
/// Starts a new transaction.
|
||||
pub async fn begin(
|
||||
&self,
|
||||
) -> Result<sqlx::Transaction<'static, Sqlite, sqlx::pool::PoolConnection<Sqlite>>> {
|
||||
let lock = self.xpool.read().await;
|
||||
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let tx = pool.begin().await?;
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or more rows.
|
||||
pub async fn query_rows<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let rows = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_all(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or more rows.
|
||||
pub async fn query_values<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let rows = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| {
|
||||
let (val,): (T,) = sqlx::FromRow::from_row(&row)?;
|
||||
Ok(val)
|
||||
})
|
||||
.fetch_all(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// 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<'a, P>(&self, statement: &'a str, params: P) -> Result<bool>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let mut rows = sqlx::query_with(statement, params).fetch(xpool);
|
||||
|
||||
match rows.next().await {
|
||||
Some(Ok(_)) => Ok(true),
|
||||
None => Ok(false),
|
||||
Some(Err(sqlx::Error::RowNotFound)) => Ok(false),
|
||||
Some(Err(err)) => Err(Error::SqlxWithContext(
|
||||
format!("exists: '{}'", statement),
|
||||
err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return one row.
|
||||
pub async fn query_row<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let row = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_one(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or one row.
|
||||
pub async fn query_row_optional<'a, T, P>(
|
||||
&self,
|
||||
statement: &'a str,
|
||||
params: P,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let row = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_optional(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn query_value_optional<'a, T, P>(
|
||||
&self,
|
||||
statement: &'a str,
|
||||
params: P,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
match self.query_row_optional(statement, params).await? {
|
||||
Some((val,)) => Ok(Some(val)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_value<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
let (val,): (T,) = self.query_row(statement, params).await?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
|
||||
self.exists(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name=?",
|
||||
paramsx![name.as_ref()],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 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<str>,
|
||||
value: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let key = key.as_ref();
|
||||
|
||||
if let Some(ref value) = value {
|
||||
let exists = self
|
||||
.exists("SELECT value FROM config WHERE keyname=?;", paramsx![key])
|
||||
.await?;
|
||||
if exists {
|
||||
self.execute(
|
||||
"UPDATE config SET value=? WHERE keyname=?;",
|
||||
paramsx![value, key],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
self.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsx![key, value],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
self.execute("DELETE FROM config WHERE keyname=?;", paramsx![key])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get configuration options from the database.
|
||||
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Option<String> {
|
||||
if !self.is_open().await || key.as_ref().is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.query_row(
|
||||
"SELECT value FROM config WHERE keyname=?;",
|
||||
paramsx![key.as_ref().to_string()],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|(res,)| res)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int(
|
||||
&self,
|
||||
context: &Context,
|
||||
key: impl AsRef<str>,
|
||||
value: i32,
|
||||
) -> Result<()> {
|
||||
self.set_raw_config(context, key, Some(&format!("{}", value)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Option<i32> {
|
||||
self.get_raw_config(key).await.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> bool {
|
||||
// Not the most obvious way to encode bool as string, but it is matter
|
||||
// of backward compatibility.
|
||||
let res = self.get_raw_config_int(key).await;
|
||||
res.unwrap_or_default() > 0
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_bool<T>(&self, context: &Context, key: T, value: bool) -> Result<()>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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<str>,
|
||||
value: i64,
|
||||
) -> Result<()> {
|
||||
self.set_raw_config(context, key, Some(&format!("{}", value)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Option<i64> {
|
||||
self.get_raw_config(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,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: impl AsRef<str>,
|
||||
) -> Result<u32> {
|
||||
// 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(),
|
||||
);
|
||||
|
||||
let res: i32 = self.query_value(&query, paramsx![value.as_ref()]).await?;
|
||||
|
||||
Ok(res as u32)
|
||||
}
|
||||
|
||||
pub async fn get_rowid2(
|
||||
&self,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: i64,
|
||||
field2: impl AsRef<str>,
|
||||
value2: i32,
|
||||
) -> Result<u32> {
|
||||
let query = format!(
|
||||
"SELECT id FROM {} WHERE {}=? AND {}=? ORDER BY id DESC",
|
||||
table.as_ref(),
|
||||
field.as_ref(),
|
||||
field2.as_ref(),
|
||||
);
|
||||
|
||||
let res: i32 = self
|
||||
.query_value(query.as_ref(), paramsx![value, value2])
|
||||
.await?;
|
||||
|
||||
Ok(res as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
let mut files_in_use = HashSet::new();
|
||||
let mut unreferenced_count = 0usize;
|
||||
|
||||
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?;
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT value FROM config;").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (row,): (String,) = row?;
|
||||
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) = prune_tombstones(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Houskeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.",);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
|
||||
let name_to_check = if let Some(namespc) = namespc_opt {
|
||||
let name_len = name.len();
|
||||
let namespc_len = namespc.len();
|
||||
if name_len <= namespc_len || !name.ends_with(namespc) {
|
||||
return false;
|
||||
}
|
||||
&name[..name_len - namespc_len]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
files_in_use.contains(name_to_check)
|
||||
}
|
||||
|
||||
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
|
||||
if !file.as_ref().starts_with("$BLOBDIR/") {
|
||||
return;
|
||||
}
|
||||
|
||||
files_in_use.insert(file.as_ref()[9..].into());
|
||||
}
|
||||
|
||||
async fn maybe_add_from_param(
|
||||
context: &Context,
|
||||
files_in_use: &mut HashSet<String>,
|
||||
query: &str,
|
||||
param_id: Param,
|
||||
) -> Result<()> {
|
||||
info!(context, "maybe_add_from_param: {}", query);
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(query).fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (row,): (String,) = row?;
|
||||
info!(context, "param: {}", &row);
|
||||
let param: Params = row.parse().unwrap_or_default();
|
||||
|
||||
if let Some(file) = param.get(param_id) {
|
||||
info!(context, "got file: {:?} {}", param_id, file);
|
||||
maybe_add_file(files_in_use, file);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn open(
|
||||
context: &Context,
|
||||
sql: &Sql,
|
||||
dbfile: impl AsRef<Path>,
|
||||
readonly: bool,
|
||||
) -> crate::error::Result<()> {
|
||||
if sql.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
"Cannot open, database \"{:?}\" already opened.",
|
||||
dbfile.as_ref(),
|
||||
);
|
||||
return Err(Error::SqlAlreadyOpen.into());
|
||||
}
|
||||
|
||||
if readonly {
|
||||
// TODO: readonly mode
|
||||
}
|
||||
|
||||
let xpool = sqlx::SqlitePool::builder()
|
||||
.min_size(1)
|
||||
.max_size(4)
|
||||
.build(&format!("sqlite://{}", dbfile.as_ref().to_string_lossy()))
|
||||
.await?;
|
||||
|
||||
{
|
||||
*sql.xpool.write().await = Some(xpool)
|
||||
}
|
||||
|
||||
if !readonly {
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update: i32 = -1;
|
||||
|
||||
if sql.table_exists("config").await? {
|
||||
exists_before_update = true;
|
||||
if let Some(version) = sql.get_raw_config_int("dbversion").await {
|
||||
dbversion_before_update = version;
|
||||
}
|
||||
}
|
||||
|
||||
// (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.
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
migrations::run(context, &sql, dbversion_before_update, exists_before_update).await?;
|
||||
|
||||
// general updates
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
// --------------------------------------------------------------------
|
||||
let mut recalc_fingerprints = false;
|
||||
let mut update_icons = false;
|
||||
|
||||
if dbversion_before_update < 34 {
|
||||
recalc_fingerprints = true;
|
||||
}
|
||||
|
||||
if dbversion_before_update < 61 {
|
||||
update_icons = true;
|
||||
}
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT addr FROM acpeerstates;").fetch(&pool);
|
||||
|
||||
while let Some(addr) = rows.next().await {
|
||||
let (addr,): (String,) = addr?;
|
||||
|
||||
if let Ok(ref mut peerstate) = Peerstate::from_addr(context, &addr).await {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(sql, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Opened {:?}.", dbfile.as_ref(),);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
DELETE FROM msgs
|
||||
WHERE (chat_id = ? OR hidden)
|
||||
AND server_uid = 0
|
||||
"#,
|
||||
paramsx![DC_CHAT_ID_TRASH as i32],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
35
src/stock.rs
35
src/stock.rs
@@ -130,9 +130,7 @@ pub enum StockMessage {
|
||||
))]
|
||||
AcSetupMsgBody = 43,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
|
||||
))]
|
||||
#[strum(props(fallback = "Cannot login as %1$s."))]
|
||||
CannotLogin = 60,
|
||||
|
||||
#[strum(props(fallback = "Could not connect to %1$s: %2$s"))]
|
||||
@@ -179,7 +177,7 @@ pub enum StockMessage {
|
||||
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
|
||||
WelcomeMessage = 71,
|
||||
|
||||
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
|
||||
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
|
||||
UnknownSenderForChat = 72,
|
||||
|
||||
#[strum(props(fallback = "Message from %1$s"))]
|
||||
@@ -187,29 +185,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Failed to send message to %1$s."))]
|
||||
FailedSendingTo = 74,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is disabled."))]
|
||||
MsgEphemeralTimerDisabled = 75,
|
||||
|
||||
// A fallback message for unknown timer values.
|
||||
// "s" stands for "second" SI unit here.
|
||||
#[strum(props(fallback = "Message deletion timer is set to %1$s s."))]
|
||||
MsgEphemeralTimerEnabled = 76,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 minute."))]
|
||||
MsgEphemeralTimerMinute = 77,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 hour."))]
|
||||
MsgEphemeralTimerHour = 78,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 day."))]
|
||||
MsgEphemeralTimerDay = 79,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 week."))]
|
||||
MsgEphemeralTimerWeek = 80,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 4 weeks."))]
|
||||
MsgEphemeralTimerFourWeeks = 81,
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -359,10 +334,10 @@ impl Context {
|
||||
let action1 = action.trim_end_matches('.');
|
||||
match from_id {
|
||||
0 => action,
|
||||
DC_CONTACT_ID_SELF => {
|
||||
1 => {
|
||||
self.stock_string_repl_str(StockMessage::MsgActionByMe, action1)
|
||||
.await
|
||||
}
|
||||
} // DC_CONTACT_ID_SELF
|
||||
_ => {
|
||||
let displayname = Contact::get_by_id(self, from_id)
|
||||
.await
|
||||
@@ -385,7 +360,7 @@ impl Context {
|
||||
|
||||
// create saved-messages chat;
|
||||
// we do this only once, if the user has deleted the chat, he can recreate it manually.
|
||||
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
|
||||
if !self.sql.get_raw_config_bool("self-chat-added").await {
|
||||
self.sql
|
||||
.set_raw_config_bool(&self, "self-chat-added", true)
|
||||
.await?;
|
||||
|
||||
@@ -27,10 +27,20 @@ impl TestContext {
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
Self { ctx, dir }
|
||||
let events = ctx.get_event_emitter();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
log::info!("{:?}", event);
|
||||
}
|
||||
});
|
||||
|
||||
TestContext { ctx, dir }
|
||||
}
|
||||
|
||||
/// Create a new configured [TestContext].
|
||||
|
||||
12
src/token.rs
12
src/token.rs
@@ -11,7 +11,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
|
||||
/// Token namespace
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[repr(i32)]
|
||||
pub enum Namespace {
|
||||
Unknown = 0,
|
||||
@@ -34,7 +34,7 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||
paramsv![namespace, foreign_id, token, time()],
|
||||
paramsx![namespace, foreign_id, &token, time()],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@@ -44,12 +44,12 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
|
||||
pub async fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
context,
|
||||
.query_value(
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
|
||||
paramsv![namespace, foreign_id],
|
||||
paramsx![namespace, foreign_id],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
|
||||
@@ -65,7 +65,7 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
|
||||
paramsv![namespace, token],
|
||||
paramsx![namespace, token],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
||||
Reference in New Issue
Block a user